55 правил С++ cpp55_wiki http://cpp55.shoutwiki.com/wiki/%D0%97%D0%B0%D0%B3%D0%BB%D0%B0%D0%B2%D0%BD%D0%B0%D1%8F_%D1%81%D1%82%D1%80%D0%B0%D0%BD%D0%B8%D1%86%D0%B0 MediaWiki 1.35.11 first-letter Медиа Служебная Обсуждение Участник Обсуждение участника 55 правил С++ Обсуждение 55 правил С++ Файл Обсуждение файла MediaWiki Обсуждение MediaWiki Шаблон Обсуждение шаблона Справка Обсуждение справки Категория Обсуждение категории Модуль Обсуждение модуля Гаджет Обсуждение гаджета Определение гаджета Обсуждение определения гаджета Заглавная страница 0 1 1 2013-06-05T13:36:57Z MediaWiki default 30443056 wikitext text/x-wiki <big>Welcome to your new site!</big><br/> This is your new site! Feel free to start editing right away! f6aad8bc2928e71e9e507f8fe0955e0ab1782cc4 3 1 2013-06-06T04:45:01Z Lerom 3360334 wikitext text/x-wiki Скотт Мэйерс Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ [[Глава 1 Приучайтесь к C++]] Правило 1: Относитесь к C++ как к конгломерату языков Что следует помнить Правило 2: Предпочитайте const, enum и inline использованию #define Что следует помнить Правило 3: Везде, где только можно используйте const Константные функции-члены Как избежать дублирования в константных и неконстантных функциях-членах Что следует помнить Правило 4: Прежде чем использовать объекты, убедитесь, что они инициализированы Что следует помнить Глава 2 Конструкторы, деструкторы и операторы присваивания Правило 5: Какие функции C++ создает и вызывает молча a29bd1be7f0c20c3e85b68559ba181dc2f05d3d9 4 3 2013-06-06T04:45:55Z Lerom 3360334 wikitext text/x-wiki Скотт Мэйерс Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ Глава 1 Приучайтесь к C++ [[Правило 1: Относитесь к C++ как к конгломерату языков]] Что следует помнить Правило 2: Предпочитайте const, enum и inline использованию #define Что следует помнить Правило 3: Везде, где только можно используйте const Константные функции-члены Как избежать дублирования в константных и неконстантных функциях-членах Что следует помнить Правило 4: Прежде чем использовать объекты, убедитесь, что они инициализированы Что следует помнить Глава 2 Конструкторы, деструкторы и операторы присваивания Правило 5: Какие функции C++ создает и вызывает молча 6cb2ffad859b16be2f42cc704824dcd6d3d36265 7 4 2013-06-06T04:56:47Z Lerom 3360334 wikitext text/x-wiki Скотт Мэйерс Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ Глава 1 Приучайтесь к C++ [[Правило 1: Относитесь к C++ как к конгломерату языков]]<br> [[Правило 2: Предпочитайте const, enum и inline использованию #define]] Что следует помнить Правило 3: Везде, где только можно используйте const Константные функции-члены Как избежать дублирования в константных и неконстантных функциях-членах Что следует помнить Правило 4: Прежде чем использовать объекты, убедитесь, что они инициализированы Что следует помнить Глава 2 Конструкторы, деструкторы и операторы присваивания Правило 5: Какие функции C++ создает и вызывает молча 034d936a1646e377d6a89a3f83691247a0e4942f 8 7 2013-06-06T04:58:09Z Lerom 3360334 wikitext text/x-wiki Скотт Мэйерс Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ === Глава 1 Приучайтесь к C++ === <br> [[Правило 1: Относитесь к C++ как к конгломерату языков]]<br> [[Правило 2: Предпочитайте const, enum и inline использованию #define]]<br> [[Правило 3: Везде, где только можно используйте const]]<br> [[Константные функции-члены]]<br> [[Как избежать дублирования в константных и неконстантных функциях-членах]]<br> Правило 4: Прежде чем использовать объекты, убедитесь, что они инициализированы Что следует помнить Глава 2 Конструкторы, деструкторы и операторы присваивания Правило 5: Какие функции C++ создает и вызывает молча 6e0830ad9685d314e632ddf6b482ab76493b25ff 9 8 2013-06-06T05:00:51Z Lerom 3360334 wikitext text/x-wiki == Скотт Мэйерс. Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ == ==== Глава 1. Приучайтесь к C++ ==== [[Правило 1: Относитесь к C++ как к конгломерату языков]]<br> [[Правило 2: Предпочитайте const, enum и inline использованию #define]]<br> [[Правило 3: Везде, где только можно используйте const]]<br> :[[Константные функции-члены]]<br> :[[Как избежать дублирования в константных и неконстантных функциях-членах]]<br> [[Правило 4: Прежде чем использовать объекты, убедитесь, что они инициализированы]]<br> ==== Глава 2. Конструкторы, деструкторы и операторы присваивания ==== [[Правило 5: Какие функции C++ создает и вызывает молча]]<br> ca9f49cbb57bf9195692de944d4a0b78ad0e938e 13 9 2013-06-06T06:51:56Z 80.76.185.8 0 /* Скотт Мэйерс. Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ */ wikitext text/x-wiki == Скотт Мэйерс. Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ == ==== Глава 1. Приучайтесь к C++ ==== [[Правило 1: Относитесь к C++ как к конгломерату языков]]<br> [[Правило 2: Предпочитайте const, enum и inline использованию #define]]<br> [[Правило 3: Везде, где только можно используйте const]]<br> :[[Константные функции-члены]]<br> :[[Как избежать дублирования в константных и неконстантных функциях-членах]]<br> [[Правило 4: Прежде чем использовать объекты, убедитесь, что они инициализированы]]<br> ==== Глава 2. Конструкторы, деструкторы и операторы присваивания ==== [[Правило 5: Какие функции C++ создает и вызывает молча]]<br> [[Правило 6: Явно запрещайте компилятору генерировать функции, которые вам не нужны]]<br> [[Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе]]<br> [[Правило 8: Не позволяйте исключениям покидать деструкторы]]<br> [[Правило 9: Никогда не вызывайте виртуальные функции в конструкторе или деструкторе]]<br> [[Правило 10: Операторы присваивания должны возвращать ссылку на *this]]<br> [[Правило 11: В operator= осуществляйте проверку на присваивание самому себе]]<br> [[Правило 12: Копируйте все части объекта]]<br> ==== Глава 3 Управление ресурсами ==== [[Правило 13: Используйте объекты для управления ресурсами]]<br> [[Правило 14: Тщательно продумывайте поведение при копировании классов, управляющих ресурсами]]<br> [[Правило 15: Предоставляйте доступ к самим ресурсам из управляющих ими классов]]<br> [[Правило 16: Используйте одинаковые формы new и delete]]<br> [[Правило 17: Помещение в «интеллектуальный» указатель объекта, вьщеленного с помощью new, лучше располагать в отдельном предложении]]<br> ==== Глава 4 Проектирование программ и объявления ==== [[Правило 18: Проектируйте интерфейсы так, что их легко было использовать правильно и трудно – неправильно]]<br> [[Правило 19: Рассматривайте проектирование класса как проектирование типа]]<br> [[Правило 20: Предпочитайте передачу по ссылке на const передаче по значению]]<br> [[Правило 21: Не пытайтесь вернуть ссылку, когда должны вернуть объект]]<br> [[Правило 22: Объявляйте данные-члены закрытыми]]<br> [[Правило 23: Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса]]<br> [[Правило 24: Объявляйте функции, не являющиеся членами, когда преобразование типов должно быть применимо ко всем параметрам]]<br> [[Правило 25: Подумайте о поддержке функции swap, не возбуждающей исключений]]<br> ==== Глава 5 Реализация ==== [[Правило 26: Откладывайте определение переменных насколько возможно]]<br> [[Правило 27: Не злоупотребляйте приведением типов]]<br> [[Правило 28: Избегайте возвращения «дескрипторов» внутренних данных]]<br> [[Правило 29: Стремитесь, чтобы программа была безопасна относительно исключений]]<br> [[Правило 30: Тщательно обдумывайте использование встроенных функций]]<br> [[Правило 31: Уменьшайте зависимости файлов при компиляции]]<br> ==== Глава 6 Наследование и объектно-ориентированное проектирование ==== [[Правило 32: Используйте открытое наследование для моделирования отношения «является»]]v [[Правило 33: Не скрывайте унаследованные имена]]<br> [[Правило 34: Различайте наследование интерфейса и наследование реализации]]<br> [[Правило 35: Рассмотрите альтернативы виртуальным функциям]]<br> :[[Реализация паттерна««Шаблонный метод» с помощью идиомы невиртуального интерфейса]]<br> :[[Реализация паттерна «Стратегия» посредством указателей на функции]]<br> :[[Реализация паттерна «Стратегия» посредством класса tr::function]]<br> :[[«Классический» паттерн «Стратегия»]]<br> :[[Резюме]]<br> [[Правило 36: Никогда не переопределяйте наследуемые невиртуальные функции]]<br> [[Правило 37: Никогда не переопределяйте наследуемое значение аргумента функции по умолчанию]]<br> [[Правило 38: Моделируйте отношение «содержит» или «реализуется посредством» с помощью композиции]]<br> [[Правило 39: Продумывайте подход к использованию закрытого наследования]]<br> [[Правило 40: Продумывайте подход к использованию множественного наследования]]<br> ==== Глава 7 Шаблоны и обобщенное программирование ==== [[Правило 41: Разберитесь в том, что такое неявные интерфейсы и полиморфизм на этапе компиляции]]<br> [[Правило 42: Усвойте оба значения ключевого слова typename]]<br> [[Правило 43: Необходимо знать, как обращаться к именам в шаблонных базовых классах]]<br> [[Правило 44: Размещайте независимый от параметров код вне шаблонов]]<br> [[Правило 45: Разрабатывайте шаблоны функций-членов так, чтобы они принимали «все совместимые типы»]]<br> [[Правило 46: Определяйте внутри шаблонов функции, не являющиеся членами, когда желательны преобразования типа]]<br> [[Правило 47: Используйте классы-характеристики для предоставления информации о типах]]<br> [[Правило 48: Изучите метапрограммирование шаблонов]]<br> ==== Глава 8 Настройка new и delete ==== [[Правило 49: Разберитесь в поведении обработчика new]]<br> [[Правило 50: Когда имеет смысл заменять new и delete]]<br> [[Правило 51: Придерживайтесь принятых соглашений при написании new и delete]]<br> [[Правило 52: Если вы написали оператор new с размещением, напишите и соответствующий оператор delete]]<br> ==== Глава 9 Разное ==== [[Правило 53: Обращайте внимание на предупреждения компилятора]]<br> [[Правило 54: Ознакомьтесь со стандартной библиотекой, включая TR1]]<br> [[Правило 55: Познакомьтесь с Boost]]<br> c0404c01f19b01417cea103f8f40f993e0fb2287 19 13 2013-06-07T12:44:44Z 80.76.185.8 0 /* Скотт Мэйерс. Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ */ wikitext text/x-wiki == Скотт Мэйерс. Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ == ==== Глава 1. Приучайтесь к C++ ==== [[Правило 1: Относитесь к C++ как к конгломерату языков]]<br> [[Правило 2: Предпочитайте const, enum и inline использованию #define]]<br> [[Правило 3: Везде, где только можно используйте const]]<br> :[[Правило 3: Везде, где только можно используйте const#Константные функции-члены | Константные функции-члены]]<br> :[[Правило 3: Везде, где только можно используйте const#Как избежать дублирования в константных и неконстантных функциях-членах | Как избежать дублирования в константных и неконстантных функциях-членах]]<br> [[Правило 4: Прежде чем использовать объекты, убедитесь, что они инициализированы]]<br> ==== Глава 2. Конструкторы, деструкторы и операторы присваивания ==== [[Правило 5: Какие функции C++ создает и вызывает молча]]<br> [[Правило 6: Явно запрещайте компилятору генерировать функции, которые вам не нужны]]<br> [[Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе]]<br> [[Правило 8: Не позволяйте исключениям покидать деструкторы]]<br> [[Правило 9: Никогда не вызывайте виртуальные функции в конструкторе или деструкторе]]<br> [[Правило 10: Операторы присваивания должны возвращать ссылку на *this]]<br> [[Правило 11: В operator= осуществляйте проверку на присваивание самому себе]]<br> [[Правило 12: Копируйте все части объекта]]<br> ==== Глава 3 Управление ресурсами ==== [[Правило 13: Используйте объекты для управления ресурсами]]<br> [[Правило 14: Тщательно продумывайте поведение при копировании классов, управляющих ресурсами]]<br> [[Правило 15: Предоставляйте доступ к самим ресурсам из управляющих ими классов]]<br> [[Правило 16: Используйте одинаковые формы new и delete]]<br> [[Правило 17: Помещение в «интеллектуальный» указатель объекта, вьщеленного с помощью new, лучше располагать в отдельном предложении]]<br> ==== Глава 4 Проектирование программ и объявления ==== [[Правило 18: Проектируйте интерфейсы так, что их легко было использовать правильно и трудно – неправильно]]<br> [[Правило 19: Рассматривайте проектирование класса как проектирование типа]]<br> [[Правило 20: Предпочитайте передачу по ссылке на const передаче по значению]]<br> [[Правило 21: Не пытайтесь вернуть ссылку, когда должны вернуть объект]]<br> [[Правило 22: Объявляйте данные-члены закрытыми]]<br> [[Правило 23: Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса]]<br> [[Правило 24: Объявляйте функции, не являющиеся членами, когда преобразование типов должно быть применимо ко всем параметрам]]<br> [[Правило 25: Подумайте о поддержке функции swap, не возбуждающей исключений]]<br> ==== Глава 5 Реализация ==== [[Правило 26: Откладывайте определение переменных насколько возможно]]<br> [[Правило 27: Не злоупотребляйте приведением типов]]<br> [[Правило 28: Избегайте возвращения «дескрипторов» внутренних данных]]<br> [[Правило 29: Стремитесь, чтобы программа была безопасна относительно исключений]]<br> [[Правило 30: Тщательно обдумывайте использование встроенных функций]]<br> [[Правило 31: Уменьшайте зависимости файлов при компиляции]]<br> ==== Глава 6 Наследование и объектно-ориентированное проектирование ==== [[Правило 32: Используйте открытое наследование для моделирования отношения «является»]]v [[Правило 33: Не скрывайте унаследованные имена]]<br> [[Правило 34: Различайте наследование интерфейса и наследование реализации]]<br> [[Правило 35: Рассмотрите альтернативы виртуальным функциям]]<br> :[[Реализация паттерна««Шаблонный метод» с помощью идиомы невиртуального интерфейса]]<br> :[[Реализация паттерна «Стратегия» посредством указателей на функции]]<br> :[[Реализация паттерна «Стратегия» посредством класса tr::function]]<br> :[[«Классический» паттерн «Стратегия»]]<br> :[[Резюме]]<br> [[Правило 36: Никогда не переопределяйте наследуемые невиртуальные функции]]<br> [[Правило 37: Никогда не переопределяйте наследуемое значение аргумента функции по умолчанию]]<br> [[Правило 38: Моделируйте отношение «содержит» или «реализуется посредством» с помощью композиции]]<br> [[Правило 39: Продумывайте подход к использованию закрытого наследования]]<br> [[Правило 40: Продумывайте подход к использованию множественного наследования]]<br> ==== Глава 7 Шаблоны и обобщенное программирование ==== [[Правило 41: Разберитесь в том, что такое неявные интерфейсы и полиморфизм на этапе компиляции]]<br> [[Правило 42: Усвойте оба значения ключевого слова typename]]<br> [[Правило 43: Необходимо знать, как обращаться к именам в шаблонных базовых классах]]<br> [[Правило 44: Размещайте независимый от параметров код вне шаблонов]]<br> [[Правило 45: Разрабатывайте шаблоны функций-членов так, чтобы они принимали «все совместимые типы»]]<br> [[Правило 46: Определяйте внутри шаблонов функции, не являющиеся членами, когда желательны преобразования типа]]<br> [[Правило 47: Используйте классы-характеристики для предоставления информации о типах]]<br> [[Правило 48: Изучите метапрограммирование шаблонов]]<br> ==== Глава 8 Настройка new и delete ==== [[Правило 49: Разберитесь в поведении обработчика new]]<br> [[Правило 50: Когда имеет смысл заменять new и delete]]<br> [[Правило 51: Придерживайтесь принятых соглашений при написании new и delete]]<br> [[Правило 52: Если вы написали оператор new с размещением, напишите и соответствующий оператор delete]]<br> ==== Глава 9 Разное ==== [[Правило 53: Обращайте внимание на предупреждения компилятора]]<br> [[Правило 54: Ознакомьтесь со стандартной библиотекой, включая TR1]]<br> [[Правило 55: Познакомьтесь с Boost]]<br> 69eb890838acaa74254dbd1878a9d0ce05d27b87 50 19 2013-06-26T13:20:37Z Lerom 3360334 wikitext text/x-wiki == Скотт Мэйерс. Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ == ==== Глава 1. Приучайтесь к C++ ==== [[Правило 1: Относитесь к C++ как к конгломерату языков]]<br> [[Правило 2: Предпочитайте const, enum и inline использованию #define]]<br> [[Правило 3: Везде, где только можно используйте const]]<br> :[[Правило 3: Везде, где только можно используйте const#Константные функции-члены | Константные функции-члены]]<br> :[[Правило 3: Везде, где только можно используйте const#Как избежать дублирования в константных и неконстантных функциях-членах | Как избежать дублирования в константных и неконстантных функциях-членах]]<br> [[Правило 4: Прежде чем использовать объекты, убедитесь, что они инициализированы]]<br> ==== Глава 2. Конструкторы, деструкторы и операторы присваивания ==== [[Правило 5: Какие функции C++ создает и вызывает молча]]<br> [[Правило 6: Явно запрещайте компилятору генерировать функции, которые вам не нужны]]<br> [[Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе]]<br> [[Правило 8: Не позволяйте исключениям покидать деструкторы]]<br> [[Правило 9: Никогда не вызывайте виртуальные функции в конструкторе или деструкторе]]<br> [[Правило 10: Операторы присваивания должны возвращать ссылку на *this]]<br> [[Правило 11: В operator= осуществляйте проверку на присваивание самому себе]]<br> [[Правило 12: Копируйте все части объекта]]<br> ==== Глава 3 Управление ресурсами ==== [[Правило 13: Используйте объекты для управления ресурсами]]<br> [[Правило 14: Тщательно продумывайте поведение при копировании классов, управляющих ресурсами]]<br> [[Правило 15: Предоставляйте доступ к самим ресурсам из управляющих ими классов]]<br> [[Правило 16: Используйте одинаковые формы new и delete]]<br> [[Правило 17: Помещение в «интеллектуальный» указатель объекта, вьщеленного с помощью new, лучше располагать в отдельном предложении]]<br> ==== Глава 4 Проектирование программ и объявления ==== [[Правило 18: Проектируйте интерфейсы так, что их легко было использовать правильно и трудно – неправильно]]<br> [[Правило 19: Рассматривайте проектирование класса как проектирование типа]]<br> [[Правило 20: Предпочитайте передачу по ссылке на const передаче по значению]]<br> [[Правило 21: Не пытайтесь вернуть ссылку, когда должны вернуть объект]]<br> [[Правило 22: Объявляйте данные-члены закрытыми]]<br> [[Правило 23: Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса]]<br> [[Правило 24: Объявляйте функции, не являющиеся членами, когда преобразование типов должно быть применимо ко всем параметрам]]<br> [[Правило 25: Подумайте о поддержке функции swap, не возбуждающей исключений]]<br> ==== Глава 5 Реализация ==== [[Правило 26: Откладывайте определение переменных насколько возможно]]<br> [[Правило 27: Не злоупотребляйте приведением типов]]<br> [[Правило 28: Избегайте возвращения «дескрипторов» внутренних данных]]<br> [[Правило 29: Стремитесь, чтобы программа была безопасна относительно исключений]]<br> [[Правило 30: Тщательно обдумывайте использование встроенных функций]]<br> [[Правило 31: Уменьшайте зависимости файлов при компиляции]]<br> ==== Глава 6 Наследование и объектно-ориентированное проектирование ==== [[Правило 32: Используйте открытое наследование для моделирования отношения «является»]]v [[Правило 33: Не скрывайте унаследованные имена]]<br> [[Правило 34: Различайте наследование интерфейса и наследование реализации]]<br> [[Правило 35: Рассмотрите альтернативы виртуальным функциям]]<br> :[[Реализация паттерна««Шаблонный метод» с помощью идиомы невиртуального интерфейса]]<br> :[[Реализация паттерна «Стратегия» посредством указателей на функции]]<br> :[[Реализация паттерна «Стратегия» посредством класса tr::function]]<br> :[[«Классический» паттерн «Стратегия»]]<br> :[[Резюме]]<br> [[Правило 36: Никогда не переопределяйте наследуемые невиртуальные функции]]<br> [[Правило 37: Никогда не переопределяйте наследуемое значение аргумента функции по умолчанию]]<br> [[Правило 38: Моделируйте отношение «содержит» или «реализуется посредством» с помощью композиции]]<br> [[Правило 39: Продумывайте подход к использованию закрытого наследования]]<br> [[Правило 40: Продумывайте подход к использованию множественного наследования]]<br> ==== Глава 7 Шаблоны и обобщенное программирование ==== [[Правило 41: Разберитесь в том, что такое неявные интерфейсы и полиморфизм на этапе компиляции]]<br> [[Правило 42: Усвойте оба значения ключевого слова typename]]<br> [[Правило 43: Необходимо знать, как обращаться к именам в шаблонных базовых классах]]<br> [[Правило 44: Размещайте независимый от параметров код вне шаблонов]]<br> [[Правило 45: Разрабатывайте шаблоны функций-членов так, чтобы они принимали «все совместимые типы»]]<br> [[Правило 46: Определяйте внутри шаблонов функции, не являющиеся членами, когда желательны преобразования типа]]<br> [[Правило 47: Используйте классы-характеристики для предоставления информации о типах]]<br> [[Правило 48: Изучите метапрограммирование шаблонов]]<br> ==== Глава 8 Настройка new и delete ==== [[Правило 49: Разберитесь в поведении обработчика new]]<br> [[Правило 50: Когда имеет смысл заменять new и delete]]<br> [[Правило 51: Придерживайтесь принятых соглашений при написании new и delete]]<br> [[Правило 52: Если вы написали оператор new с размещением, напишите и соответствующий оператор delete]]<br> ==== Глава 9 Разное ==== [[Правило 53: Обращайте внимание на предупреждения компилятора]]<br> [[Правило 54: Ознакомьтесь со стандартной библиотекой, включая TR1]]<br> [[Правило 55: Познакомьтесь с Boost]]<br> 09dc69b9764de2274eb7c60aefbcb738bfd36ba4 Обсуждение участника:Lerom 3 2 2 2013-06-05T13:36:57Z ShoutWiki 11 wikitext text/x-wiki Hi Lerom, thank you for choosing ShoutWiki to make your wiki. We would suggest that you start your wiki off by doing these few basic things: *Upload a logo. You can do this by uploading an image over [[:File:Wiki.png]]. (not available on some skins) *Design your [[Main Page]]. The main page is likely the first thing users will see. It should be attractive and catch the eye. *Start building content. All wikis need content to become the best they can be. If you need help with making a logo, skin or favicon, please see [[s:w:logocreation|ShoutWiki's Logo Creation Wiki]]. If you need any help with making your wiki, feel free to contact [[s:ShoutWiki Staff|ShoutWiki staff]] either via their talk pages or via [[Special:Contact]]. Alternatively, you can talk to us, or other users, via [[s:ShoutWiki Hub:IRC|IRC]]. Thank you again for using ShoutWiki. [[s:ShoutWiki Staff|ShoutWiki staff]] 13:36, 5 июня 2013 288aed23eb25363e3477f5f21caeccc351ca76d4 Правило 1: Относитесь к C++ как к конгломерату языков 0 3 5 2013-06-06T04:47:43Z Lerom 3360334 Новая страница: «Поначалу C++ был просто языком C с добавлением некоторых объектно-ориентированных средст…» wikitext text/x-wiki Поначалу C++ был просто языком C с добавлением некоторых объектно-ориентированных средств. Даже первоначальное название C++ («C с классами») отражает эту связь. По мере того как язык становился все более зрелым, он рос и развивался, в него включались идеи и стратегии программирования, выходящие за рамки C с классами. Исключения потребовали другого подхода к структурированию функций (см. правило 29). Шаблоны изменили наши представления о проектировании программ (см. правило 41), а библиотека STL определила подход к расширяемости, который никто ранее не мог себе представить. Сегодня C++ – это язык программирования с несколькими парадигмами, поддерживающий процедурное, объектно-ориентированное, функциональное, обобщенное и метапрограммирование. Эти мощь и гибкость делают C++ несравненным инструментом, однако могут привести в замешательство. У любой рекомендации по «правильному применению» есть исключения. Как найти смысл в таком языке? Лучше всего воспринимать C++ не как один язык, а как конгломерат взаимосвязанных языков. В пределах отдельного подъязыка правила достаточно просты, понятны и легко запоминаются. Однако когда вы переходите от одного подъязыка к другому, правила могут изменяться. Чтобы увидеть смысл в C++, вы должны распознавать его основные подъязыки. К счастью, их всего четыре: *'''C'''. В глубине своей C++ все еще основан на C. Блоки, предложения, препроцессор, встроенные типы данных, массивы, указатели и т. п. – все это пришло из C. Во многих случаях C++ предоставляет для решения тех или иных задач более развитые механизмы, чем C (пример см. в правиле 2 – альтернатива препроцессору и 13 – применение объектов для управления ресурсами), но когда вы начнете работать с той частью C++, которая имеет аналоги в C, то поймете, что правила эффективного программирования отражают более ограниченный характер языка C: никаких шаблонов, никаких исключений, никакой перегрузки и т. д. *'''Объектно-ориентированный C++'''. Эта часть C++ представляет то, чем был «C с классами», включая конструкторы и деструкторы, инкапсуляцию, наследование, полиморфизм, виртуальные функции (динамическое связывание) и т. д. Это та часть C++, к которой в наибольшей степени применимы классические правила объектно-ориентированного проектирования. *'''C++ с шаблонами'''. Эта часть C++ называется обобщенным программированием, о ней большинство программистов знают мало. Шаблоны теперь пронизывают C++ снизу доверху, и признаком хорошего тона в программировании уже стало включение конструкций, немыслимых без шаблонов (например, см. правило 46 о преобразовании типов при вызовах шаблонных функций). Фактически шаблоны, благодаря своей мощи, породили совершенно новую парадигму программирования: метапрограммирование шаблонов (template metaprogramming – TMP). В правиле 48 представлен обзор TMP, но если вы не являетесь убежденным фанатиком шаблонов, у вас нет причин чрезмерно задумываться об этом. TMP не отнесешь к самым распространенным приемам программирования на C++. *'''STL'''. STL – это, конечно, библиотека шаблонов, но очень специализированная. Принятые в ней соглашения относительно контейнеров, итераторов, алгоритмов и функциональных объектов великолепно сочетаются между собой, но шаблоны и библиотеки можно строить и по-другому. Работая с библиотекой STL, вы обязаны следовать ее соглашениям. Помните об этих четырех подъязыках и не удивляйтесь, если попадете в ситуацию, когда соображения эффективности программирования потребуют от вас менять стратегию при переключении с одного подъязыка на другой. Например, для встроенных типов (в стиле C) передача параметров по значению в общем случае более эффективна, чем передача по ссылке, но если вы программируете в объектно-ориентированном стиле, то из-за наличия определенных пользователем конструкторов и деструкторов передача по ссылке на константу обычно становится более эффективной. В особенности это относится к подъязыку «C++ с шаблонами», потому что там вы обычно даже не знаете заранее типа объектов, с которыми имеете дело. Но вот вы перешли к использованию STL, и опять старое правило C о передаче по значению становится актуальным, потому что итераторы и функциональные объекты смоделированы через указатели C. (Подробно о выборе способа передачи параметров см. правило 20.) Таким образом, C++ не является однородным языком с единственным набором правил. Это – конгломерат подъязыков, каждый со своими собственными соглашениями. Если вы будете помнить об этих подъязыках, то обнаружите, что понять C++ намного проще. 66fe65230556cf6eb476138c6d4bfc356446ea46 6 5 2013-06-06T04:55:38Z Lerom 3360334 wikitext text/x-wiki Поначалу C++ был просто языком C с добавлением некоторых объектно-ориентированных средств. Даже первоначальное название C++ («C с классами») отражает эту связь. По мере того как язык становился все более зрелым, он рос и развивался, в него включались идеи и стратегии программирования, выходящие за рамки C с классами. Исключения потребовали другого подхода к структурированию функций ([[см. правило 29]]). Шаблоны изменили наши представления о проектировании программ ([[см. правило 41]]), а библиотека STL определила подход к расширяемости, который никто ранее не мог себе представить. Сегодня C++ – это язык программирования с несколькими парадигмами, поддерживающий процедурное, объектно-ориентированное, функциональное, обобщенное и метапрограммирование. Эти мощь и гибкость делают C++ несравненным инструментом, однако могут привести в замешательство. У любой рекомендации по «правильному применению» есть исключения. Как найти смысл в таком языке? Лучше всего воспринимать C++ не как один язык, а как конгломерат взаимосвязанных языков. В пределах отдельного подъязыка правила достаточно просты, понятны и легко запоминаются. Однако когда вы переходите от одного подъязыка к другому, правила могут изменяться. Чтобы увидеть смысл в C++, вы должны распознавать его основные подъязыки. К счастью, их всего четыре: *'''C'''. В глубине своей C++ все еще основан на C. Блоки, предложения, препроцессор, встроенные типы данных, массивы, указатели и т. п. – все это пришло из C. Во многих случаях C++ предоставляет для решения тех или иных задач более развитые механизмы, чем C (пример [[см. в правиле 2]] – альтернатива препроцессору и 13 – применение объектов для управления ресурсами), но когда вы начнете работать с той частью C++, которая имеет аналоги в C, то поймете, что правила эффективного программирования отражают более ограниченный характер языка C: никаких шаблонов, никаких исключений, никакой перегрузки и т. д. *'''Объектно-ориентированный C++'''. Эта часть C++ представляет то, чем был «C с классами», включая конструкторы и деструкторы, инкапсуляцию, наследование, полиморфизм, виртуальные функции (динамическое связывание) и т. д. Это та часть C++, к которой в наибольшей степени применимы классические правила объектно-ориентированного проектирования. *'''C++ с шаблонами'''. Эта часть C++ называется обобщенным программированием, о ней большинство программистов знают мало. Шаблоны теперь пронизывают C++ снизу доверху, и признаком хорошего тона в программировании уже стало включение конструкций, немыслимых без шаблонов (например, [[см. правило 46]] о преобразовании типов при вызовах шаблонных функций). Фактически шаблоны, благодаря своей мощи, породили совершенно новую парадигму программирования: метапрограммирование шаблонов (template metaprogramming – TMP). В [[правиле 48]] представлен обзор TMP, но если вы не являетесь убежденным фанатиком шаблонов, у вас нет причин чрезмерно задумываться об этом. TMP не отнесешь к самым распространенным приемам программирования на C++. *'''STL'''. STL – это, конечно, библиотека шаблонов, но очень специализированная. Принятые в ней соглашения относительно контейнеров, итераторов, алгоритмов и функциональных объектов великолепно сочетаются между собой, но шаблоны и библиотеки можно строить и по-другому. Работая с библиотекой STL, вы обязаны следовать ее соглашениям. Помните об этих четырех подъязыках и не удивляйтесь, если попадете в ситуацию, когда соображения эффективности программирования потребуют от вас менять стратегию при переключении с одного подъязыка на другой. Например, для встроенных типов (в стиле C) передача параметров по значению в общем случае более эффективна, чем передача по ссылке, но если вы программируете в объектно-ориентированном стиле, то из-за наличия определенных пользователем конструкторов и деструкторов передача по ссылке на константу обычно становится более эффективной. В особенности это относится к подъязыку «C++ с шаблонами», потому что там вы обычно даже не знаете заранее типа объектов, с которыми имеете дело. Но вот вы перешли к использованию STL, и опять старое правило C о передаче по значению становится актуальным, потому что итераторы и функциональные объекты смоделированы через указатели C. (Подробно о выборе способа передачи параметров [[см. правило 20.]]) Таким образом, C++ не является однородным языком с единственным набором правил. Это – конгломерат подъязыков, каждый со своими собственными соглашениями. Если вы будете помнить об этих подъязыках, то обнаружите, что понять C++ намного проще. == Что следует помнить == *Правила эффективного программирования меняются в зависимости от части C++, которую вы используете b82ab09bd64424f116a98e519d4368e5fd8ebc32 14 6 2013-06-06T06:55:59Z 80.76.185.8 0 wikitext text/x-wiki Поначалу C++ был просто языком C с добавлением некоторых объектно-ориентированных средств. Даже первоначальное название C++ («C с классами») отражает эту связь. По мере того как язык становился все более зрелым, он рос и развивался, в него включались идеи и стратегии программирования, выходящие за рамки C с классами. Исключения потребовали другого подхода к структурированию функций ([[Правило 29: Стремитесь, чтобы программа была безопасна относительно исключений|см. правило 29]]). Шаблоны изменили наши представления о проектировании программ ([[Правило 41: Разберитесь в том, что такое неявные интерфейсы и полиморфизм на этапе компиляции|см. правило 41]]), а библиотека STL определила подход к расширяемости, который никто ранее не мог себе представить. Сегодня C++ – это язык программирования с несколькими парадигмами, поддерживающий процедурное, объектно-ориентированное, функциональное, обобщенное и метапрограммирование. Эти мощь и гибкость делают C++ несравненным инструментом, однако могут привести в замешательство. У любой рекомендации по «правильному применению» есть исключения. Как найти смысл в таком языке? Лучше всего воспринимать C++ не как один язык, а как конгломерат взаимосвязанных языков. В пределах отдельного подъязыка правила достаточно просты, понятны и легко запоминаются. Однако когда вы переходите от одного подъязыка к другому, правила могут изменяться. Чтобы увидеть смысл в C++, вы должны распознавать его основные подъязыки. К счастью, их всего четыре: *'''C'''. В глубине своей C++ все еще основан на C. Блоки, предложения, препроцессор, встроенные типы данных, массивы, указатели и т. п. – все это пришло из C. Во многих случаях C++ предоставляет для решения тех или иных задач более развитые механизмы, чем C (пример [[см. в правиле 2]] – альтернатива препроцессору и 13 – применение объектов для управления ресурсами), но когда вы начнете работать с той частью C++, которая имеет аналоги в C, то поймете, что правила эффективного программирования отражают более ограниченный характер языка C: никаких шаблонов, никаких исключений, никакой перегрузки и т. д. *'''Объектно-ориентированный C++'''. Эта часть C++ представляет то, чем был «C с классами», включая конструкторы и деструкторы, инкапсуляцию, наследование, полиморфизм, виртуальные функции (динамическое связывание) и т. д. Это та часть C++, к которой в наибольшей степени применимы классические правила объектно-ориентированного проектирования. *'''C++ с шаблонами'''. Эта часть C++ называется обобщенным программированием, о ней большинство программистов знают мало. Шаблоны теперь пронизывают C++ снизу доверху, и признаком хорошего тона в программировании уже стало включение конструкций, немыслимых без шаблонов (например, [[см. правило 46]] о преобразовании типов при вызовах шаблонных функций). Фактически шаблоны, благодаря своей мощи, породили совершенно новую парадигму программирования: метапрограммирование шаблонов (template metaprogramming – TMP). В [[правиле 48]] представлен обзор TMP, но если вы не являетесь убежденным фанатиком шаблонов, у вас нет причин чрезмерно задумываться об этом. TMP не отнесешь к самым распространенным приемам программирования на C++. *'''STL'''. STL – это, конечно, библиотека шаблонов, но очень специализированная. Принятые в ней соглашения относительно контейнеров, итераторов, алгоритмов и функциональных объектов великолепно сочетаются между собой, но шаблоны и библиотеки можно строить и по-другому. Работая с библиотекой STL, вы обязаны следовать ее соглашениям. Помните об этих четырех подъязыках и не удивляйтесь, если попадете в ситуацию, когда соображения эффективности программирования потребуют от вас менять стратегию при переключении с одного подъязыка на другой. Например, для встроенных типов (в стиле C) передача параметров по значению в общем случае более эффективна, чем передача по ссылке, но если вы программируете в объектно-ориентированном стиле, то из-за наличия определенных пользователем конструкторов и деструкторов передача по ссылке на константу обычно становится более эффективной. В особенности это относится к подъязыку «C++ с шаблонами», потому что там вы обычно даже не знаете заранее типа объектов, с которыми имеете дело. Но вот вы перешли к использованию STL, и опять старое правило C о передаче по значению становится актуальным, потому что итераторы и функциональные объекты смоделированы через указатели C. (Подробно о выборе способа передачи параметров [[см. правило 20.]]) Таким образом, C++ не является однородным языком с единственным набором правил. Это – конгломерат подъязыков, каждый со своими собственными соглашениями. Если вы будете помнить об этих подъязыках, то обнаружите, что понять C++ намного проще. == Что следует помнить == *Правила эффективного программирования меняются в зависимости от части C++, которую вы используете 2cc9a2d18f208079232406f4b0cea058058cfa49 23 14 2013-06-07T13:49:04Z 80.76.185.8 0 wikitext text/x-wiki <P>По мере того как язык становился все более зрелым, он рос и развивался, в него включались идеи и стратегии программирования, выходящие за рамки C с классами. Исключения потребовали другого подхода к структурированию функций (см. правило 29). Шаблоны изменили наши представления о проектировании программ (см. правило 41), а библиотека STL определила подход к расширяемости, который никто ранее не мог себе представить.</P> <P>Сегодня C++ – это язык <EM>программирования с несколькими парадигмами,</EM> поддерживающий процедурное, объектно-ориентированное, функциональное, обобщенное и метапрограммирование. Эти мощь и гибкость делают C++ несравненным инструментом, однако могут привести в замешательство. У любой рекомендации по «правильному применению» есть исключения. Как найти смысл в таком языке?</P> <P>Лучше всего воспринимать C++ не как один язык, а как конгломерат взаимосвязанных языков. В пределах отдельного подъязыка правила достаточно просты, понятны и легко запоминаются. Однако когда вы переходите от одного подъязыка к другому, правила могут изменяться. Чтобы увидеть смысл в C++, вы должны распознавать его основные подъязыки. К счастью, их всего четыре:</P> <P><STRONG>• C.</STRONG> В глубине своей C++ все еще основан на C. Блоки, предложения, препроцессор, встроенные типы данных, массивы, указатели и т. п. – все это пришло из C. Во многих случаях C++ предоставляет для решения тех или иных задач более развитые механизмы, чем C (пример см. в правиле 2 – альтернатива препроцессору и 13 – применение объектов для управления ресурсами), но когда вы начнете работать с той частью C++, которая имеет аналоги в C, то поймете, что правила эффективного программирования отражают более ограниченный характер языка C: никаких шаблонов, никаких исключений, никакой перегрузки и т. д.</P> <P>• <STRONG>Объектно-ориентированный C++.</STRONG> Эта часть C++ представляет то, чем был «C с классами», включая конструкторы и деструкторы, инкапсуляцию, наследование, полиморфизм, виртуальные функции (динамическое связывание) и т. д. Это та часть C++, к которой в наибольшей степени применимы классические правила объектно-ориентированного проектирования.</P> <P>• <STRONG>C++ с шаблонами.</STRONG> Эта часть C++ называется обобщенным программированием, о ней большинство программистов знают мало. Шаблоны теперь пронизывают C++ снизу доверху, и признаком хорошего тона в программировании уже стало включение конструкций, немыслимых без шаблонов (например, см. правило 46 о преобразовании типов при вызовах шаблонных функций). Фактически шаблоны, благодаря своей мощи, породили совершенно новую парадигму программирования: <EM>метапрограммирование шаблонов</EM> (template metaprogramming – TMP). В правиле 48 представлен обзор TMP, но если вы не являетесь убежденным фанатиком шаблонов, у вас нет причин чрезмерно задумываться об этом. TMP не отнесешь к самым распространенным приемам программирования на C++.</P> <P>• <STRONG>STL.</STRONG> STL – это, конечно, библиотека шаблонов, но очень специализированная. Принятые в ней соглашения относительно контейнеров, итераторов, алгоритмов и функциональных объектов великолепно сочетаются между собой, но шаблоны и библиотеки можно строить и по-другому. Работая с библиотекой STL, вы обязаны следовать ее соглашениям.</P> <P>Помните об этих четырех подъязыках и не удивляйтесь, если попадете в ситуацию, когда соображения эффективности программирования потребуют от вас менять стратегию при переключении с одного подъязыка на другой. Например, для встроенных типов (в стиле C) передача параметров по значению в общем случае более эффективна, чем передача по ссылке, но если вы программируете в объектно-ориентированном стиле, то из-за наличия определенных пользователем конструкторов и деструкторов передача по ссылке на константу обычно становится более эффективной. В особенности это относится к подъязыку «C++ с шаблонами», потому что там вы обычно даже не знаете заранее типа объектов, с которыми имеете дело. Но вот вы перешли к использованию STL, и опять старое правило C о передаче по значению становится актуальным, потому что итераторы и функциональные объекты смоделированы через указатели C. (Подробно о выборе способа передачи параметров см. правило 20.)</P> <P>Таким образом, C++ не является однородным языком с единственным набором правил. Это – конгломерат подъязыков, каждый со своими собственными соглашениями. Если вы будете помнить об этих подъязыках, то обнаружите, что понять C++ намного проще.</P> == Что следует помнить == *Правила эффективного программирования меняются в зависимости от части C++, которую вы используете 9ed12646ada812c01c6766e52e848d44969da61d 24 23 2013-06-07T13:53:04Z 80.76.185.8 0 wikitext text/x-wiki <P>По мере того как язык становился все более зрелым, он рос и развивался, в него включались идеи и стратегии программирования, выходящие за рамки C с классами. Исключения потребовали другого подхода к структурированию функций ([[Правило 29: Стремитесь, чтобы программа была безопасна относительно исключений | см. правило 29]]). Шаблоны изменили наши представления о проектировании программ ([[Правило 41: Разберитесь в том, что такое неявные интерфейсы и полиморфизм на этапе компиляции | см. правило 41]]), а библиотека STL определила подход к расширяемости, который никто ранее не мог себе представить.</P> <P>Сегодня C++ – это язык <EM>программирования с несколькими парадигмами,</EM> поддерживающий процедурное, объектно-ориентированное, функциональное, обобщенное и метапрограммирование. Эти мощь и гибкость делают C++ несравненным инструментом, однако могут привести в замешательство. У любой рекомендации по «правильному применению» есть исключения. Как найти смысл в таком языке?</P> <P>Лучше всего воспринимать C++ не как один язык, а как конгломерат взаимосвязанных языков. В пределах отдельного подъязыка правила достаточно просты, понятны и легко запоминаются. Однако когда вы переходите от одного подъязыка к другому, правила могут изменяться. Чтобы увидеть смысл в C++, вы должны распознавать его основные подъязыки. К счастью, их всего четыре:</P> <P><STRONG>• C.</STRONG> В глубине своей C++ все еще основан на C. Блоки, предложения, препроцессор, встроенные типы данных, массивы, указатели и т. п. – все это пришло из C. Во многих случаях C++ предоставляет для решения тех или иных задач более развитые механизмы, чем C (пример см. в правиле 2 – альтернатива препроцессору и [[Правило 13: Используйте объекты для управления ресурсами | 13 – применение объектов для управления ресурсами]]), но когда вы начнете работать с той частью C++, которая имеет аналоги в C, то поймете, что правила эффективного программирования отражают более ограниченный характер языка C: никаких шаблонов, никаких исключений, никакой перегрузки и т. д.</P> <P>• <STRONG>Объектно-ориентированный C++.</STRONG> Эта часть C++ представляет то, чем был «C с классами», включая конструкторы и деструкторы, инкапсуляцию, наследование, полиморфизм, виртуальные функции (динамическое связывание) и т. д. Это та часть C++, к которой в наибольшей степени применимы классические правила объектно-ориентированного проектирования.</P> <P>• <STRONG>C++ с шаблонами.</STRONG> Эта часть C++ называется обобщенным программированием, о ней большинство программистов знают мало. Шаблоны теперь пронизывают C++ снизу доверху, и признаком хорошего тона в программировании уже стало включение конструкций, немыслимых без шаблонов (например, [[Правило 46: Определяйте внутри шаблонов функции, не являющиеся членами, когда желательны преобразования типа | см. правило 46]] о преобразовании типов при вызовах шаблонных функций). Фактически шаблоны, благодаря своей мощи, породили совершенно новую парадигму программирования: <EM>метапрограммирование шаблонов</EM> (template metaprogramming – TMP). В [[Правило 48: Изучите метапрограммирование шаблонов | правиле 48]] представлен обзор TMP, но если вы не являетесь убежденным фанатиком шаблонов, у вас нет причин чрезмерно задумываться об этом. TMP не отнесешь к самым распространенным приемам программирования на C++.</P> <P>• <STRONG>STL.</STRONG> STL – это, конечно, библиотека шаблонов, но очень специализированная. Принятые в ней соглашения относительно контейнеров, итераторов, алгоритмов и функциональных объектов великолепно сочетаются между собой, но шаблоны и библиотеки можно строить и по-другому. Работая с библиотекой STL, вы обязаны следовать ее соглашениям.</P> <P>Помните об этих четырех подъязыках и не удивляйтесь, если попадете в ситуацию, когда соображения эффективности программирования потребуют от вас менять стратегию при переключении с одного подъязыка на другой. Например, для встроенных типов (в стиле C) передача параметров по значению в общем случае более эффективна, чем передача по ссылке, но если вы программируете в объектно-ориентированном стиле, то из-за наличия определенных пользователем конструкторов и деструкторов передача по ссылке на константу обычно становится более эффективной. В особенности это относится к подъязыку «C++ с шаблонами», потому что там вы обычно даже не знаете заранее типа объектов, с которыми имеете дело. Но вот вы перешли к использованию STL, и опять старое правило C о передаче по значению становится актуальным, потому что итераторы и функциональные объекты смоделированы через указатели C. (Подробно о выборе способа передачи параметров [[Правило 20: Предпочитайте передачу по ссылке на const передаче по значению | см. правило 20]].)</P> <P>Таким образом, C++ не является однородным языком с единственным набором правил. Это – конгломерат подъязыков, каждый со своими собственными соглашениями. Если вы будете помнить об этих подъязыках, то обнаружите, что понять C++ намного проще.</P> == Что следует помнить == *Правила эффективного программирования меняются в зависимости от части C++, которую вы используете 774340d73e42c60ee57abaa730fac74625991d4a Правило 2: Предпочитайте const, enum и inline использованию 0 4 10 2013-06-06T05:12:50Z Lerom 3360334 Новая страница: «Это правило лучше было бы назвать «Компилятор предпочтительнее препроцессора», посколь…» wikitext text/x-wiki Это правило лучше было бы назвать «Компилятор предпочтительнее препроцессора», поскольку #define зачастую вообще не относят к языку C++. В этом и заключается проблема. Рассмотрим простой пример; попробуйте написать что-нибудь вроде: <big><source lang="cpp"> #define ASPECT_RATIO 1.653 </source></big> Символическое имя ASPECT_RATIO может так и остаться неизвестным компилятору или быть удалено препроцессором до того, как код поступит на обработку компилятору. Если это произойдет, то имя ASPECT_RATIO не попадет в таблицу символов. Поэтому в ходе компиляции вы получите ошибку (в сообщении о ней будет упомянуто значение 1.653, а не ASPECT_RATIO). Это вызовет путаницу. Если имя ASPECT_RATIO было определено в заголовочном файле, который писали не вы, то вы вообще не будете знать, откуда взялось значение 1.653, и на поиски ответа потратите много времени. Та же проблема может возникнуть и при отладке, поскольку выбранное вами имя будет отсутствовать в таблице символов. Решение состоит в замене макроса константой: <big><source lang="cpp"> const double AspectRatio = 1.653; // имена, записанные большими буквами, // обычно применяются для макросов, // поэтому мы решили его изменить </source></big> Будучи языковой константой, AspectRatio видима компилятору и, естественно, помещается в таблицу символов. К тому же в случае использования константы с плавающей точкой (как в этом примере) генерируется более компактный код, чем при использовании #define. Дело в том, что препроцессор, слепо подставляя вместо макроса ASPECT_RATIO величину 1.653, создает множество копий 1.653 в объектном коде, в то время как использование константы никогда не породит более одной копии этого значения. При замене #define константами нужно помнить о двух особых случаях. Первый касается константных указателей. Поскольку определения констант обычно помещаются в заголовочные файлы (где к ним получает доступ множество различных исходных файлов), важно, чтобы сам указатель был объявлен с ключевым словом const, в дополнение к объявлению const того, на что он указывает. Например, чтобы объявить в заголовочном файле константную строку типа char*, слово const нужно написать дважды: <big><source lang="cpp"> const char * const authorName = “Scott Meyers”; </source></big> Более подробно о сущности и применений слова const, особенно в связке с указателями, [[см. в правиле 3]]. Но уже сейчас стоит напомнить, что объекты типа string обычно предпочтительнее своих прародителей – строк типа char *, поэтому authorName лучше определить так: <big><source lang="cpp"> const std::string authorName(“Scott Meyers”); </source></big> Второе замечание касается констант, объявляемых в составе класса. Чтобы ограничить область действия константы классом, необходимо сделать ее членом класса, и чтобы гарантировать, что существует только одна копия константы, требуется сделать ее статическим членом: <big><source lang="cpp"> class GamePlayer { private: static const int NumTurns = 5; // объявление константы int scores[NumTurns]; // использование константы ... }; </source></big> То, что вы видите выше, – это объявление NumTurns, а не ее определение. Обычно C++ требует, чтобы вы представляли определение для всего, что используете, но объявленные в классе константы, которые являются статическими и имеют встроенный тип (то есть целые, символьные, булевские) – это исключение из правил. До тех пор пока вы не пытаетесь получить адрес такой константы, можете объявлять и использовать ее без предоставления определения. Если же вам нужно получить адрес либо если ваш компилятор настаивает на наличии определения, то можете написать что-то подобное: <big><source lang="cpp"> const int GamePlayer::NumTurns; // определение NumTurns; см. ниже, // почему не указывается значение </source></big> Поместите этот код в файл реализации, а не в заголовочный файл. Поскольку начальное значение константы класса представлено там, где она объявлена (то есть NumTurns инициализировано значением 5 при объявлении), то в точке определения задавать начальное значение не требуется. Отметим, кстати, что нет возможности объявить в классе константу посредством #define, потому что #define не учитывает области действия. Как только макрос определен, он остается в силе для всей оставшейся части компилируемого кода (если только где-то ниже не встретится #undef). Это значит, что директива #define неприменима не только для объявления констант в классе, но вообще не может быть использована для обеспечения какой бы то ни было инкапсуляции, то есть придать смысл выражению «private #define» невозможно. В то же время константные данные-члены могут быть инкапсулированы, примером может служить NumTurns. Старые компиляторы могут не поддерживать показанный выше синтаксис, так как в более ранних версиях языка было запрещено задавать значения статических членов класса во время объявления. Более того, инициализация в классе допускалась только для целых типов и для констант. Если вышеприведенный синтаксис не работает, то начальное значение следует задавать в определении: <big><source lang="cpp"> class CostEstimate { private: static const double FudgeFactor; // объявление статической константы ... // класса – помещается в файл заголовка }; const double // определение статической константы CostEstimate::FudgeFactor = 1.35; // класса – помещается в файл реализации </source></big> Обычно ничего больше и не требуется. Единственное исключение обнаруживается тогда, когда для компиляции класса необходима константа. Например, при объявлении массива GamePlayer::scores компилятору нужно знать размер массива. Чтобы работать с компилятором, ошибочно запрещающим инициализировать статические целые константы внутри класса, можно воспользоваться способом, известным под названием «трюка с перечислением». Он основан на том, что переменные перечисляемого типа можно использовать там, где ожидаются значения типа int, поэтому GamePlayer можно определить так: <big><source lang="cpp"> class GamePlayer { private: enum ( NumTurns = 5 }; // “трюк с перечислением” – делает из // NumTurns символ со значением 5 int scores[NumTurns]; // нормально ... }; </source></big> Этот прием стоит знать по нескольким причинам. Во-первых, поведение «трюка с перечислением» в некоторых отношениях более похоже на #define, чем на константу, а иногда это как раз то, что нужно. Например, можно получить адрес константы, но нельзя получить адрес перечисления, как нельзя получить и адрес #define. Если вы хотите запретить получать адрес или ссылку на какую-нибудь целую константу, то применение enum – хороший способ наложить такое ограничение. (Подробнее о поддержке проектных ограничений с помощью приемов кодирования можно узнать из [[правила 18]]). К тому же, хотя хорошие компиляторы не выделяют память для константных объектов целых типов (если только вы не создаете указателя или ссылки на объект), менее изощренные могут так поступать, а вам это, возможно, ни к чему. Как и #define, перечисления никогда не станут причиной подобного нежелательного распределения памяти. Вторая причина знать о «трюке с перечислением» чисто прагматическая. Он используется в очень многих программах, поэтому нужно уметь распознавать этот трюк, когда вы с ним сталкиваетесь. Вообще говоря, этот прием – фундаментальная техника, применяемая при метапрограммировании шаблонов ([[см. правило 48]]). Вернемся к препроцессору. Другой частый случай неправильного использования директивы #define – создание макросов, которые выглядят как функции, но не обременены накладными расходов, связанными с вызовом функций. Ниже представлен макрос, который вызывает некоторую функцию f c аргументом, равным максимальному из двух значений: <big><source lang="cpp"> // вызвать f, передав ей максимум из a и b #define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b)) </source></big> В этой строчке содержится так много недостатков, что даже не совсем понятно, с какого начать. Всякий раз при написании подобного макроса вы должны помнить о том, что все аргументы следует заключать в скобки. В противном случае вы рискуете столкнуться с проблемой, когда кто-нибудь вызовет его с выражением в качестве аргумента. Но даже если вы сделаете все правильно, посмотрите, какие странные вещи могут произойти: <big><source lang="cpp"> int a = 5, b = 0; CALL_WITH_MAX(++a, b); // a увеличивается дважды CALL_WITH_MAX(++a, b+10); // a увеличивается один раз </source></big> Происходящее внутри max зависит от того, с чем она сравнивается! К счастью, вы нет нужды мириться с поведением, так сильно противоречащим привычной логике. Существует метод, позволяющий добиться такой же эффективности, как при использовании препроцессора. Но при этом обеспечивается как предсказуемость поведения, так и контроль типов аргументов (что характерно для обычных функций). Этот результат достигается применением шаблона встроенной (inline) функции ([[см. правило 30]]): <big><source lang="cpp"> template<typename T> inline void callWithMax(const T& a, const T& b) // Поскольку мы не знаем, { // что есть T, то передаем f(a > b ? a : b); // его по ссылке на const - } // см. параграф 20 </source></big> Этот шаблон генерирует целое семейство функций, каждая из которых принимает два аргумента одного и того же типа и вызывает f с наибольшим из них. Нет необходимости заключать параметры в скобки внутри тела функции, не нужно заботиться о многократном вычислении параметров и т. д. Более того, поскольку callWithMax – настоящая функция, на нее распространяются правила областей действия и контроля доступа. Например, можно говорить о встроенной функции, являющейся закрытым членом класса. Описать нечто подобное с помощью макроса невозможно. Наличие const, enum и inline резко снижает потребность в препроцессоре (особенно это относится к #define), но не устраняет ее полностью. Директива #include остается существенной, а #ifdef/#ifndef продолжают играть важную роль в управлении компиляцией. Пока еще не время отказываться от препроцессора, но определенно стоит задуматься, как избавиться от него в дальнейшем. e551b1080600e7d8149560529ffad8f42bcdbbc3 11 10 2013-06-06T05:16:32Z Lerom 3360334 wikitext text/x-wiki Это правило лучше было бы назвать «Компилятор предпочтительнее препроцессора», поскольку #define зачастую вообще не относят к языку C++. В этом и заключается проблема. Рассмотрим простой пример; попробуйте написать что-нибудь вроде: <source lang="cpp"> #define ASPECT_RATIO 1.653 </source> Символическое имя ASPECT_RATIO может так и остаться неизвестным компилятору или быть удалено препроцессором до того, как код поступит на обработку компилятору. Если это произойдет, то имя ASPECT_RATIO не попадет в таблицу символов. Поэтому в ходе компиляции вы получите ошибку (в сообщении о ней будет упомянуто значение 1.653, а не ASPECT_RATIO). Это вызовет путаницу. Если имя ASPECT_RATIO было определено в заголовочном файле, который писали не вы, то вы вообще не будете знать, откуда взялось значение 1.653, и на поиски ответа потратите много времени. Та же проблема может возникнуть и при отладке, поскольку выбранное вами имя будет отсутствовать в таблице символов. Решение состоит в замене макроса константой: <source lang="cpp"> const double AspectRatio = 1.653; // имена, записанные большими буквами, // обычно применяются для макросов, // поэтому мы решили его изменить </source> Будучи языковой константой, AspectRatio видима компилятору и, естественно, помещается в таблицу символов. К тому же в случае использования константы с плавающей точкой (как в этом примере) генерируется более компактный код, чем при использовании #define. Дело в том, что препроцессор, слепо подставляя вместо макроса ASPECT_RATIO величину 1.653, создает множество копий 1.653 в объектном коде, в то время как использование константы никогда не породит более одной копии этого значения. При замене #define константами нужно помнить о двух особых случаях. Первый касается константных указателей. Поскольку определения констант обычно помещаются в заголовочные файлы (где к ним получает доступ множество различных исходных файлов), важно, чтобы сам указатель был объявлен с ключевым словом const, в дополнение к объявлению const того, на что он указывает. Например, чтобы объявить в заголовочном файле константную строку типа char*, слово const нужно написать дважды: <source lang="cpp"> const char * const authorName = “Scott Meyers”; </source> Более подробно о сущности и применений слова const, особенно в связке с указателями, [[см. в правиле 3]]. Но уже сейчас стоит напомнить, что объекты типа string обычно предпочтительнее своих прародителей – строк типа char *, поэтому authorName лучше определить так: <source lang="cpp"> const std::string authorName(“Scott Meyers”); </source> Второе замечание касается констант, объявляемых в составе класса. Чтобы ограничить область действия константы классом, необходимо сделать ее членом класса, и чтобы гарантировать, что существует только одна копия константы, требуется сделать ее статическим членом: <source lang="cpp"> class GamePlayer { private: static const int NumTurns = 5; // объявление константы int scores[NumTurns]; // использование константы ... }; </source> То, что вы видите выше, – это объявление NumTurns, а не ее определение. Обычно C++ требует, чтобы вы представляли определение для всего, что используете, но объявленные в классе константы, которые являются статическими и имеют встроенный тип (то есть целые, символьные, булевские) – это исключение из правил. До тех пор пока вы не пытаетесь получить адрес такой константы, можете объявлять и использовать ее без предоставления определения. Если же вам нужно получить адрес либо если ваш компилятор настаивает на наличии определения, то можете написать что-то подобное: <source lang="cpp"> const int GamePlayer::NumTurns; // определение NumTurns; см. ниже, // почему не указывается значение </source> Поместите этот код в файл реализации, а не в заголовочный файл. Поскольку начальное значение константы класса представлено там, где она объявлена (то есть NumTurns инициализировано значением 5 при объявлении), то в точке определения задавать начальное значение не требуется. Отметим, кстати, что нет возможности объявить в классе константу посредством #define, потому что #define не учитывает области действия. Как только макрос определен, он остается в силе для всей оставшейся части компилируемого кода (если только где-то ниже не встретится #undef). Это значит, что директива #define неприменима не только для объявления констант в классе, но вообще не может быть использована для обеспечения какой бы то ни было инкапсуляции, то есть придать смысл выражению «private #define» невозможно. В то же время константные данные-члены могут быть инкапсулированы, примером может служить NumTurns. Старые компиляторы могут не поддерживать показанный выше синтаксис, так как в более ранних версиях языка было запрещено задавать значения статических членов класса во время объявления. Более того, инициализация в классе допускалась только для целых типов и для констант. Если вышеприведенный синтаксис не работает, то начальное значение следует задавать в определении: <source lang="cpp"> class CostEstimate { private: static const double FudgeFactor; // объявление статической константы ... // класса – помещается в файл заголовка }; const double // определение статической константы CostEstimate::FudgeFactor = 1.35; // класса – помещается в файл реализации </source> Обычно ничего больше и не требуется. Единственное исключение обнаруживается тогда, когда для компиляции класса необходима константа. Например, при объявлении массива GamePlayer::scores компилятору нужно знать размер массива. Чтобы работать с компилятором, ошибочно запрещающим инициализировать статические целые константы внутри класса, можно воспользоваться способом, известным под названием «трюка с перечислением». Он основан на том, что переменные перечисляемого типа можно использовать там, где ожидаются значения типа int, поэтому GamePlayer можно определить так: <source lang="cpp"> class GamePlayer { private: enum ( NumTurns = 5 }; // “трюк с перечислением” – делает из // NumTurns символ со значением 5 int scores[NumTurns]; // нормально ... }; </source> Этот прием стоит знать по нескольким причинам. Во-первых, поведение «трюка с перечислением» в некоторых отношениях более похоже на #define, чем на константу, а иногда это как раз то, что нужно. Например, можно получить адрес константы, но нельзя получить адрес перечисления, как нельзя получить и адрес #define. Если вы хотите запретить получать адрес или ссылку на какую-нибудь целую константу, то применение enum – хороший способ наложить такое ограничение. (Подробнее о поддержке проектных ограничений с помощью приемов кодирования можно узнать из [[правила 18]]). К тому же, хотя хорошие компиляторы не выделяют память для константных объектов целых типов (если только вы не создаете указателя или ссылки на объект), менее изощренные могут так поступать, а вам это, возможно, ни к чему. Как и #define, перечисления никогда не станут причиной подобного нежелательного распределения памяти. Вторая причина знать о «трюке с перечислением» чисто прагматическая. Он используется в очень многих программах, поэтому нужно уметь распознавать этот трюк, когда вы с ним сталкиваетесь. Вообще говоря, этот прием – фундаментальная техника, применяемая при метапрограммировании шаблонов ([[см. правило 48]]). Вернемся к препроцессору. Другой частый случай неправильного использования директивы #define – создание макросов, которые выглядят как функции, но не обременены накладными расходов, связанными с вызовом функций. Ниже представлен макрос, который вызывает некоторую функцию f c аргументом, равным максимальному из двух значений: <source lang="cpp"> // вызвать f, передав ей максимум из a и b #define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b)) </source> В этой строчке содержится так много недостатков, что даже не совсем понятно, с какого начать. Всякий раз при написании подобного макроса вы должны помнить о том, что все аргументы следует заключать в скобки. В противном случае вы рискуете столкнуться с проблемой, когда кто-нибудь вызовет его с выражением в качестве аргумента. Но даже если вы сделаете все правильно, посмотрите, какие странные вещи могут произойти: <source lang="cpp"> int a = 5, b = 0; CALL_WITH_MAX(++a, b); // a увеличивается дважды CALL_WITH_MAX(++a, b+10); // a увеличивается один раз </source> Происходящее внутри max зависит от того, с чем она сравнивается! К счастью, вы нет нужды мириться с поведением, так сильно противоречащим привычной логике. Существует метод, позволяющий добиться такой же эффективности, как при использовании препроцессора. Но при этом обеспечивается как предсказуемость поведения, так и контроль типов аргументов (что характерно для обычных функций). Этот результат достигается применением шаблона встроенной (inline) функции ([[см. правило 30]]): <source lang="cpp"> template<typename T> inline void callWithMax(const T& a, const T& b) // Поскольку мы не знаем, { // что есть T, то передаем f(a > b ? a : b); // его по ссылке на const - } // см. параграф 20 </source> Этот шаблон генерирует целое семейство функций, каждая из которых принимает два аргумента одного и того же типа и вызывает f с наибольшим из них. Нет необходимости заключать параметры в скобки внутри тела функции, не нужно заботиться о многократном вычислении параметров и т. д. Более того, поскольку callWithMax – настоящая функция, на нее распространяются правила областей действия и контроля доступа. Например, можно говорить о встроенной функции, являющейся закрытым членом класса. Описать нечто подобное с помощью макроса невозможно. Наличие const, enum и inline резко снижает потребность в препроцессоре (особенно это относится к #define), но не устраняет ее полностью. Директива #include остается существенной, а #ifdef/#ifndef продолжают играть важную роль в управлении компиляцией. Пока еще не время отказываться от препроцессора, но определенно стоит задуматься, как избавиться от него в дальнейшем. == Что следует помнить == *Для простых констант директиве #define следует предпочесть константные объекты и перечисления (enum). *Вместо имитирующих функции макросов, определенных через #define, лучше применять встроенные функции. 5c2fbdf26da617c90573d738a69ea23a0f59e696 Правило 3: Везде, где только можно используйте const 0 5 12 2013-06-06T05:24:07Z Lerom 3360334 Новая страница: «Замечательное свойство модификатора const состоит в том, что он накладывает определенное …» wikitext text/x-wiki Замечательное свойство модификатора const состоит в том, что он накладывает определенное семантическое ограничение: данный объект не должен модифицироваться, – и компилятор будет проводить это ограничение в жизнь. const позволяет указать компилятору и программистам, что определенная величина должна оставаться неизменной. Во всех подобных случаях вы должны обозначить это явным образом, призывая себе на помощь компилятор и гарантируя тем самым, что ограничение не будет нарушено. Ключевое слово const удивительно многосторонне. Вне классов вы можете использовать его для определения констант в глобальной области или в пространстве имен ([[см. правило 2]]), а также для статических объектов (внутри файла, функции или блока). Внутри классов допустимо применять его как для статических, так и для нестатических данных-членов. Для указателей можно специфицировать, должен ли быть константным сам указатель, данные, на которые он указывает, либо и то, и другое (или ни то, ни другое): <source lang="cpp"> char greeting[] = “Hello”; char *p = greeting; // неконстантный указатель, // неконстантные данные const char *p = greeting; // неконстантный указатель, // константные данные char * const p = greeting; // константный указатель, // неконстантные данные const char * const p = greeting; // константный указатель, // константные данные </source> Этот синтаксис не так страшен, как может показаться. Если слово const появляется слева от звездочки, константным является то, на что указывает указатель; если справа, то сам указатель является константным. Наконец, если же слово const появляется с обеих сторон, то константно и то, и другое. Когда то, на что указывается, – константа, некоторые программисты ставят const перед идентификатором типа. Другие – после идентификатора типа, но перед звездочкой. Семантической разницы здесь нет, поэтому следующие функции принимают параметр одного и того же типа: <source lang="cpp"> void f1(const Widget *pw); // f1 принимает указатель на // константный объект Widget void f2(Widget const *pw); // то же самое делает f2 </source> Поскольку в реальном коде встречаются обе формы, следует привыкать и к той, и к другой. Итераторы STL смоделированы на основе указателей, поэтому iterator ведет себя почти как указатель T*. Объявление const-итератора подобно объявлению const-указателя (то есть записи T* const): итератор не может начать указывать на что-то другое, но то, на что он указывает, может быть модифицировано. Если вы хотите иметь итератор, который указывал бы на нечто, что запрещено модифицировать (то есть STL-аналог указателя const T*), то вам понадобится константный итератор: <source lang="cpp"> std::vector vec; ... const std::vector::iterator iter = // iter работает как T* const vec.begin(); *iter = 10; // Ok, изменяется то, на что // указывает iter ++iter; // ошибка! iter константный std::vector::const_iterator citer = // citer работает как const T* vec.begin(); *citer = 10; // ошибка! *citer константный ++citer; // нормально, citer изменяется </source> Некоторые из наиболее интересных применений const связаны с объявлениями функций. В этом случае const может относиться к возвращаемому функцией значению, к отдельным параметрам, а для функций-членов – еще и к функции в целом. Если указать в объявлении функции, что она возвращает константное значение, то можно уменьшить количество ошибок в клиентских программах, не снижая уровня безопасности и эффективности. Например, рассмотрим объявление функции operator* для рациональных чисел, введенное в [[правиле 24]]: <source lang="cpp"> class Rational {…} const Rational operator*(const Rational& lhs, const Rational& rhs); </source> Многие программисты удивятся, впервые увидев такое объявление. Почему результат функции operator* должен быть константным объектом? Потому что в противном случае пользователь получил бы возможность делать вещи, которые иначе как надругательством над здравым смыслом не назовешь: <source lang="cpp"> Rational a, b, c; … (a*b)=c; // присваивание произведению a*b! </source> Я не знаю, с какой стати программисту пришло бы в голову присваивать значение произведению двух чисел, но могу точно сказать, что иногда такое может случиться по недосмотру. Достаточно простой опечатки (при условии, что тип может быть преобразован к bool): <source lang="cpp"> if (a*b = c)... // имелось в виду сравнение! </source> Такой код был бы совершенно некорректным, если бы a и b имели встроенный тип. Одним из критериев качества пользовательских типов является совместимость со встроенными (см. также правило 18), а возможность присваивания значения результату произведения двух объектов представляется мне весьма далекой от совместимости. Если же объявить, что operator* возвращает константное значение, то такая ситуация станет невозможной. Вот почему Так Следует Поступать. В отношении аргументов с модификатором const трудно сказать что-то новое; они ведут себя как локальные константные const-объекты. Всюду, где возможно, добавляйте этот модификатор. Если модифицировать аргумент или локальный объект нет необходимости, объявите его как const. Вам всего-то придется набрать шесть символов, зато это предотвратит досадные ошибки типа «хотел напечатать ==, а нечаянно напечатал =» (к чему это приводит, мы только что видели). 51a6dd5a5f4ef9cb9c30a8d8e5512b601c8111c5 16 12 2013-06-07T12:08:05Z 80.76.185.8 0 wikitext text/x-wiki Замечательное свойство модификатора const состоит в том, что он накладывает определенное семантическое ограничение: данный объект не должен модифицироваться, – и компилятор будет проводить это ограничение в жизнь. const позволяет указать компилятору и программистам, что определенная величина должна оставаться неизменной. Во всех подобных случаях вы должны обозначить это явным образом, призывая себе на помощь компилятор и гарантируя тем самым, что ограничение не будет нарушено. Ключевое слово const удивительно многосторонне. Вне классов вы можете использовать его для определения констант в глобальной области или в пространстве имен ([[см. правило 2]]), а также для статических объектов (внутри файла, функции или блока). Внутри классов допустимо применять его как для статических, так и для нестатических данных-членов. Для указателей можно специфицировать, должен ли быть константным сам указатель, данные, на которые он указывает, либо и то, и другое (или ни то, ни другое): <source lang="cpp"> char greeting[] = “Hello”; char *p = greeting; // неконстантный указатель, // неконстантные данные const char *p = greeting; // неконстантный указатель, // константные данные char * const p = greeting; // константный указатель, // неконстантные данные const char * const p = greeting; // константный указатель, // константные данные </source> Этот синтаксис не так страшен, как может показаться. Если слово const появляется слева от звездочки, константным является то, на что указывает указатель; если справа, то сам указатель является константным. Наконец, если же слово const появляется с обеих сторон, то константно и то, и другое. Когда то, на что указывается, – константа, некоторые программисты ставят const перед идентификатором типа. Другие – после идентификатора типа, но перед звездочкой. Семантической разницы здесь нет, поэтому следующие функции принимают параметр одного и того же типа: <source lang="cpp"> void f1(const Widget *pw); // f1 принимает указатель на // константный объект Widget void f2(Widget const *pw); // то же самое делает f2 </source> Поскольку в реальном коде встречаются обе формы, следует привыкать и к той, и к другой. Итераторы STL смоделированы на основе указателей, поэтому iterator ведет себя почти как указатель T*. Объявление const-итератора подобно объявлению const-указателя (то есть записи T* const): итератор не может начать указывать на что-то другое, но то, на что он указывает, может быть модифицировано. Если вы хотите иметь итератор, который указывал бы на нечто, что запрещено модифицировать (то есть STL-аналог указателя const T*), то вам понадобится константный итератор: <source lang="cpp"> std::vector vec; ... const std::vector::iterator iter = // iter работает как T* const vec.begin(); *iter = 10; // Ok, изменяется то, на что // указывает iter ++iter; // ошибка! iter константный std::vector::const_iterator citer = // citer работает как const T* vec.begin(); *citer = 10; // ошибка! *citer константный ++citer; // нормально, citer изменяется </source> Некоторые из наиболее интересных применений const связаны с объявлениями функций. В этом случае const может относиться к возвращаемому функцией значению, к отдельным параметрам, а для функций-членов – еще и к функции в целом. Если указать в объявлении функции, что она возвращает константное значение, то можно уменьшить количество ошибок в клиентских программах, не снижая уровня безопасности и эффективности. Например, рассмотрим объявление функции operator* для рациональных чисел, введенное в [[правиле 24]]: <source lang="cpp"> class Rational {…} const Rational operator*(const Rational& lhs, const Rational& rhs); </source> Многие программисты удивятся, впервые увидев такое объявление. Почему результат функции operator* должен быть константным объектом? Потому что в противном случае пользователь получил бы возможность делать вещи, которые иначе как надругательством над здравым смыслом не назовешь: <source lang="cpp"> Rational a, b, c; … (a*b)=c; // присваивание произведению a*b! </source> Я не знаю, с какой стати программисту пришло бы в голову присваивать значение произведению двух чисел, но могу точно сказать, что иногда такое может случиться по недосмотру. Достаточно простой опечатки (при условии, что тип может быть преобразован к bool): <source lang="cpp"> if (a*b = c)... // имелось в виду сравнение! </source> Такой код был бы совершенно некорректным, если бы a и b имели встроенный тип. Одним из критериев качества пользовательских типов является совместимость со встроенными (см. также правило 18), а возможность присваивания значения результату произведения двух объектов представляется мне весьма далекой от совместимости. Если же объявить, что operator* возвращает константное значение, то такая ситуация станет невозможной. Вот почему Так Следует Поступать. В отношении аргументов с модификатором const трудно сказать что-то новое; они ведут себя как локальные константные const-объекты. Всюду, где возможно, добавляйте этот модификатор. Если модифицировать аргумент или локальный объект нет необходимости, объявите его как const. Вам всего-то придется набрать шесть символов, зато это предотвратит досадные ошибки типа «хотел напечатать ==, а нечаянно напечатал =» (к чему это приводит, мы только что видели). == Константные функции-члены == Назначение модификатора const в объявлении функций-членов – определить, какие из них можно вызывать для константных объектов. Такие функции-члены важны по двум причинам. Во-первых, они облегчают понимание интерфейса класса, ведь полезно сразу видеть, какие функции могут модифицировать объект, а какие нет. Во-вторых, они обеспечивают возможность работать с константными объектами. Это очень важно для написания эффективного кода, потому что, как объясняется в [[Правило 20: Предпочитайте передачу по ссылке на const передаче по значению | правиле 20]], один из основных способов повысить производительность программ на C++ – передавать объекты по ссылке на константу. Но эта техника будет работать только в случае, когда функции-члены для манипулирования константными объектами объявлены с модификатором const. Многие упускают из виду, что функции, отличающиеся только наличием const в объявлении, могут быть перегружены. Это, однако, важное свойство C++. Рассмотрим класс, представляющий блок текста: <source lang="cpp"> class TextBlock { public: ... const char& operator[](std::size_t position) const // operator[] для {return text[position];} // константных объектов char& operator[](std::size_t position) // operator[] для {return text[position];} // неконстантных объектов private: std::string text; }; </source> Функцию operator[] в классе TextBlock можно использовать следующим образом: <source lang="cpp"> TextBlock tb(“Hello”); Std::cout << tb[0]; // вызов неконстантного // оператора TextBlock::operator[] const TextBlock ctb(“World”); Std::cout << ctb[0]; // вызов константного // оператора TextBlock::operator[] </source> Кстати, константные объекты чаще всего встречаются в реальных программах в результате передачи по указателю или ссылке на константу. Приведенный выше пример ctb является довольно искусственным. Но вот вам более реалистичный: <source lang="cpp"> void print(const TextBlock& ctb) // в этой функции ctb – ссылка // на константный объект { std::cout << ctb[0]; // вызов const TextBlock::operator[] ... } </source> Перегружая operator[] и создавая различные версии с разными возвращаемыми типами, вы можете по-разному обрабатывать константные и неконстантные объекты TextBlock: <source lang="cpp"> std::cout << tb[0]; // нормально – читается // неконстантный TextBlock tb[0] = ‘x’; // нормально – пишется // неконстантный TextBlock std::cout << ctb[0]; // нормально – читается // константный TextBlock ctb[0] = ‘x’; // ошибка! – запись // константного TextBlock </source> Отметим, что ошибка здесь связана только с типом значения, возвращаемого operator[]; сам вызов operator[] проходит нормально. Причина ошибки – в попытке присвоить значение объекту типа const char&, потому что это именно такой тип возвращается константной версией operator[]. Отметим также, что тип, возвращаемый неконстантной версией operator[], – это ссылка на char, а не сам char. Если бы operator[] возвращал просто char, то следующее предложение не скомпилировалось бы: <source lang="cpp"> tb[0] = ‘x’; </source> Это объясняется тем, что возвращаемое функцией значение встроенного типа модифицировать некорректно. Даже если бы это было допустимо, тот факт, что C++ возвращает объекты по значению ([[Правило 20: Предпочитайте передачу по ссылке на const передаче по значению | см. правило 20]]), означал бы следующее: модифицировалась копия tb.text[0], а не само значение tb.text[0]. Вряд ли это то, чего вы ожидаете. Давайте немного передохнем и пофилософствуем. Что означает для функции-члена быть константной? Существует два широко распространенных понятия: побитовая константность (также известная как физическая константность) и логическая константность. Сторонники побитовой константности полагают, что функция-член константна тогда и только тогда, когда она не модифицирует никакие данные-члены объекта (за исключением статических), то есть не модифицирует ни одного бита внутри объекта. Определение побитовой константности хорошо тем, что ее нарушение легко обнаружить: компилятор просто ищет присваивания членам класса. Фактически, побитовая константность – это константность, определенная в C++: функция-член с модификатором const не может модифицировать нестатические данные-члены объекта, для которого она вызвана. К сожалению, многие функции-члены, которые ведут себя далеко не константно, проходят побитовый тест. В частности, функция-член, которая модифицирует то, на что указывает указатель, часто не ведет себя как константная. Но если объекту принадлежит только указатель, то функция формально является побитово константной, и компилятор не станет возражать. Это может привести к неожиданному поведению. Например, предположим, что есть класс подобный Text-Block, где данные хранятся в строках типа char * вместо string, поскольку это необходимо для передачи в функции, написанные на языке C, который не понимает, что такое объекты типа string. <source lang="cpp"> class CtextBlock { public: ... char& operator[](std::size_t position) const // неудачное (но побитово { return pText[position]} // константное) // объявление operator[] private: char *pText; }; </source> В этом классе функция operator[] (неправильно!) объявлена как константная функция-член, хотя она возвращает ссылку на внутренние данные объекта (эта тема обсуждается [[Правило 28: Избегайте возвращения «дескрипторов» внутренних данных | в правиле 28]]). Оставим это пока в стороне и отметим, что реализация operator[] никак не модифицирует pText. В результате компилятор спокойно сгенерирует код для функции operator[]. Ведь она действительно является побитово константной, а это все, что компилятор может проверить. Но посмотрите, что происходит: <source lang="cpp"> const CtextBlock cctb(“Hello”); // объявление константного объекта char &pc = &cctb[0]; // вызов const operator[] для получения // указателя на данные cctb *pc = ‘j’; // cctb теперь имеет значение “Jello” </source> Несомненно, есть что-то некорректное в том, что вы создаете константный объект с определенным значением, вызываете для него только константную функцию-член и тем не менее изменяете его значение! Это приводит нас к понятию логической константности. Сторонники этой философии утверждают, что функции-члены с const могут модифицировать некоторые биты вызвавшего их объекта, но только так, чтобы пользователь не мог этого обнаружить. Например, ваш класс CTextBlock мог бы кэшировать длину текстового блока при каждом запросе: <source lang="cpp"> Class CtextBlock { public: ... std::size_t length() const; private: char *pText; std::size_t textLength; // последнее вычисленное значение длины // текстового блока bool lengthIsValid; // корректна ли длина в данный момент }; std::size_t CtextBlock::length() const { if(!lengthIsValid) { textLength = std::strlen(pText); // ошибка! Нельзя присваивать lengthIsValid = true; // значение textLength и } // lengthIsValid в константной // функции-члене return textLength; } </source> Эта реализация length(), конечно же, не является побитово константной, поскольку может модифицировать значения членов textLength и lengthlsValid. Но в то же время со стороны кажется, что константности объектов CTextBlock это не угрожает. Однако компилятор не согласен. Он настаивает на побитовой константности. Что делать? Решение простое: используйте модификатор mutable. Он освобождает нестатические данные-члены от ограничений побитовой константности: <source lang="cpp"> Class CtextBlock { public: ... std::size_t length() const; private: char *pText; mutable std::size_t textLength; // Эти данные-члены всегда могут быть mutable bool lengthIsValid; // модифицированы, даже в константных }; // функциях-членах std::size_t CtextBlock::length() const { if(!lengthIsValid) { textLength = std::strlen(pText); // теперь порядок lengthIsValid = true; // здесь то же } return textLength; } </source> 5e2ca131682d2bb4d24af9e14db7d2871215cb4b 17 16 2013-06-07T12:20:15Z 80.76.185.8 0 wikitext text/x-wiki Замечательное свойство модификатора const состоит в том, что он накладывает определенное семантическое ограничение: данный объект не должен модифицироваться, – и компилятор будет проводить это ограничение в жизнь. const позволяет указать компилятору и программистам, что определенная величина должна оставаться неизменной. Во всех подобных случаях вы должны обозначить это явным образом, призывая себе на помощь компилятор и гарантируя тем самым, что ограничение не будет нарушено.<br> Ключевое слово const удивительно многосторонне. Вне классов вы можете использовать его для определения констант в глобальной области или в пространстве имен ([[см. правило 2]]), а также для статических объектов (внутри файла, функции или блока). Внутри классов допустимо применять его как для статических, так и для нестатических данных-членов. Для указателей можно специфицировать, должен ли быть константным сам указатель, данные, на которые он указывает, либо и то, и другое (или ни то, ни другое): <source lang="cpp"> char greeting[] = “Hello”; char *p = greeting; // неконстантный указатель, // неконстантные данные const char *p = greeting; // неконстантный указатель, // константные данные char * const p = greeting; // константный указатель, // неконстантные данные const char * const p = greeting; // константный указатель, // константные данные </source> Этот синтаксис не так страшен, как может показаться. Если слово const появляется слева от звездочки, константным является то, на что указывает указатель; если справа, то сам указатель является константным. Наконец, если же слово const появляется с обеих сторон, то константно и то, и другое.<br> Когда то, на что указывается, – константа, некоторые программисты ставят const перед идентификатором типа. Другие – после идентификатора типа, но перед звездочкой. Семантической разницы здесь нет, поэтому следующие функции принимают параметр одного и того же типа: <source lang="cpp"> void f1(const Widget *pw); // f1 принимает указатель на // константный объект Widget void f2(Widget const *pw); // то же самое делает f2 </source> Поскольку в реальном коде встречаются обе формы, следует привыкать и к той, и к другой.<br> Итераторы STL смоделированы на основе указателей, поэтому iterator ведет себя почти как указатель T*. Объявление const-итератора подобно объявлению const-указателя (то есть записи T* const): итератор не может начать указывать на что-то другое, но то, на что он указывает, может быть модифицировано. Если вы хотите иметь итератор, который указывал бы на нечто, что запрещено модифицировать (то есть STL-аналог указателя const T*), то вам понадобится константный итератор: <source lang="cpp"> std::vector vec; ... const std::vector::iterator iter = // iter работает как T* const vec.begin(); *iter = 10; // Ok, изменяется то, на что // указывает iter ++iter; // ошибка! iter константный std::vector::const_iterator citer = // citer работает как const T* vec.begin(); *citer = 10; // ошибка! *citer константный ++citer; // нормально, citer изменяется </source> Некоторые из наиболее интересных применений const связаны с объявлениями функций. В этом случае const может относиться к возвращаемому функцией значению, к отдельным параметрам, а для функций-членов – еще и к функции в целом.<br> Если указать в объявлении функции, что она возвращает константное значение, то можно уменьшить количество ошибок в клиентских программах, не снижая уровня безопасности и эффективности. Например, рассмотрим объявление функции operator* для рациональных чисел, введенное в [[правиле 24]]: <source lang="cpp"> class Rational {…} const Rational operator*(const Rational& lhs, const Rational& rhs); </source> Многие программисты удивятся, впервые увидев такое объявление. Почему результат функции operator* должен быть константным объектом? Потому что в противном случае пользователь получил бы возможность делать вещи, которые иначе как надругательством над здравым смыслом не назовешь: <source lang="cpp"> Rational a, b, c; … (a*b)=c; // присваивание произведению a*b! </source> Я не знаю, с какой стати программисту пришло бы в голову присваивать значение произведению двух чисел, но могу точно сказать, что иногда такое может случиться по недосмотру. Достаточно простой опечатки (при условии, что тип может быть преобразован к bool): <source lang="cpp"> if (a*b = c)... // имелось в виду сравнение! </source> Такой код был бы совершенно некорректным, если бы a и b имели встроенный тип. Одним из критериев качества пользовательских типов является совместимость со встроенными (см. также правило 18), а возможность присваивания значения результату произведения двух объектов представляется мне весьма далекой от совместимости. Если же объявить, что operator* возвращает константное значение, то такая ситуация станет невозможной. Вот почему Так Следует Поступать.<br> В отношении аргументов с модификатором const трудно сказать что-то новое; они ведут себя как локальные константные const-объекты. Всюду, где возможно, добавляйте этот модификатор. Если модифицировать аргумент или локальный объект нет необходимости, объявите его как const. Вам всего-то придется набрать шесть символов, зато это предотвратит досадные ошибки типа «хотел напечатать ==, а нечаянно напечатал =» (к чему это приводит, мы только что видели). == Константные функции-члены == Назначение модификатора const в объявлении функций-членов – определить, какие из них можно вызывать для константных объектов. Такие функции-члены важны по двум причинам. Во-первых, они облегчают понимание интерфейса класса, ведь полезно сразу видеть, какие функции могут модифицировать объект, а какие нет. Во-вторых, они обеспечивают возможность работать с константными объектами. Это очень важно для написания эффективного кода, потому что, как объясняется в [[Правило 20: Предпочитайте передачу по ссылке на const передаче по значению | правиле 20]], один из основных способов повысить производительность программ на C++ – передавать объекты по ссылке на константу. Но эта техника будет работать только в случае, когда функции-члены для манипулирования константными объектами объявлены с модификатором const.<br> Многие упускают из виду, что функции, отличающиеся только наличием const в объявлении, могут быть перегружены. Это, однако, важное свойство C++. Рассмотрим класс, представляющий блок текста: <source lang="cpp"> class TextBlock { public: ... const char& operator[](std::size_t position) const // operator[] для {return text[position];} // константных объектов char& operator[](std::size_t position) // operator[] для {return text[position];} // неконстантных объектов private: std::string text; }; </source> Функцию operator[] в классе TextBlock можно использовать следующим образом: <source lang="cpp"> TextBlock tb(“Hello”); Std::cout << tb[0]; // вызов неконстантного // оператора TextBlock::operator[] const TextBlock ctb(“World”); Std::cout << ctb[0]; // вызов константного // оператора TextBlock::operator[] </source> Кстати, константные объекты чаще всего встречаются в реальных программах в результате передачи по указателю или ссылке на константу. Приведенный выше пример ctb является довольно искусственным. Но вот вам более реалистичный: <source lang="cpp"> void print(const TextBlock& ctb) // в этой функции ctb – ссылка // на константный объект { std::cout << ctb[0]; // вызов const TextBlock::operator[] ... } </source> Перегружая operator[] и создавая различные версии с разными возвращаемыми типами, вы можете по-разному обрабатывать константные и неконстантные объекты TextBlock: <source lang="cpp"> std::cout << tb[0]; // нормально – читается // неконстантный TextBlock tb[0] = ‘x’; // нормально – пишется // неконстантный TextBlock std::cout << ctb[0]; // нормально – читается // константный TextBlock ctb[0] = ‘x’; // ошибка! – запись // константного TextBlock </source> Отметим, что ошибка здесь связана только с типом значения, возвращаемого operator[]; сам вызов operator[] проходит нормально. Причина ошибки – в попытке присвоить значение объекту типа const char&, потому что это именно такой тип возвращается константной версией operator[]. Отметим также, что тип, возвращаемый неконстантной версией operator[], – это ссылка на char, а не сам char. Если бы operator[] возвращал просто char, то следующее предложение не скомпилировалось бы: <source lang="cpp"> tb[0] = ‘x’; </source> Это объясняется тем, что возвращаемое функцией значение встроенного типа модифицировать некорректно. Даже если бы это было допустимо, тот факт, что C++ возвращает объекты по значению ([[Правило 20: Предпочитайте передачу по ссылке на const передаче по значению | см. правило 20]]), означал бы следующее: модифицировалась копия tb.text[0], а не само значение tb.text[0]. Вряд ли это то, чего вы ожидаете.<br> Давайте немного передохнем и пофилософствуем. Что означает для функции-члена быть константной? Существует два широко распространенных понятия: побитовая константность (также известная как физическая константность) и логическая константность.<br> Сторонники побитовой константности полагают, что функция-член константна тогда и только тогда, когда она не модифицирует никакие данные-члены объекта (за исключением статических), то есть не модифицирует ни одного бита внутри объекта. Определение побитовой константности хорошо тем, что ее нарушение легко обнаружить: компилятор просто ищет присваивания членам класса. Фактически, побитовая константность – это константность, определенная в C++: функция-член с модификатором const не может модифицировать нестатические данные-члены объекта, для которого она вызвана. К сожалению, многие функции-члены, которые ведут себя далеко не константно, проходят побитовый тест. В частности, функция-член, которая модифицирует то, на что указывает указатель, часто не ведет себя как константная. Но если объекту принадлежит только указатель, то функция формально является побитово константной, и компилятор не станет возражать. Это может привести к неожиданному поведению. Например, предположим, что есть класс подобный Text-Block, где данные хранятся в строках типа char * вместо string, поскольку это необходимо для передачи в функции, написанные на языке C, который не понимает, что такое объекты типа string. <source lang="cpp"> class CtextBlock { public: ... char& operator[](std::size_t position) const // неудачное (но побитово { return pText[position]} // константное) // объявление operator[] private: char *pText; }; </source> В этом классе функция operator[] (неправильно!) объявлена как константная функция-член, хотя она возвращает ссылку на внутренние данные объекта (эта тема обсуждается [[Правило 28: Избегайте возвращения «дескрипторов» внутренних данных | в правиле 28]]). Оставим это пока в стороне и отметим, что реализация operator[] никак не модифицирует pText. В результате компилятор спокойно сгенерирует код для функции operator[]. Ведь она действительно является побитово константной, а это все, что компилятор может проверить. Но посмотрите, что происходит: <source lang="cpp"> const CtextBlock cctb(“Hello”); // объявление константного объекта char &pc = &cctb[0]; // вызов const operator[] для получения // указателя на данные cctb *pc = ‘j’; // cctb теперь имеет значение “Jello” </source> Несомненно, есть что-то некорректное в том, что вы создаете константный объект с определенным значением, вызываете для него только константную функцию-член и тем не менее изменяете его значение!<br> Это приводит нас к понятию логической константности. Сторонники этой философии утверждают, что функции-члены с const могут модифицировать некоторые биты вызвавшего их объекта, но только так, чтобы пользователь не мог этого обнаружить. Например, ваш класс CTextBlock мог бы кэшировать длину текстового блока при каждом запросе: <source lang="cpp"> Class CtextBlock { public: ... std::size_t length() const; private: char *pText; std::size_t textLength; // последнее вычисленное значение длины // текстового блока bool lengthIsValid; // корректна ли длина в данный момент }; std::size_t CtextBlock::length() const { if(!lengthIsValid) { textLength = std::strlen(pText); // ошибка! Нельзя присваивать lengthIsValid = true; // значение textLength и } // lengthIsValid в константной // функции-члене return textLength; } </source> Эта реализация length(), конечно же, не является побитово константной, поскольку может модифицировать значения членов textLength и lengthlsValid. Но в то же время со стороны кажется, что константности объектов CTextBlock это не угрожает. Однако компилятор не согласен. Он настаивает на побитовой константности. Что делать? Решение простое: используйте модификатор mutable. Он освобождает нестатические данные-члены от ограничений побитовой константности: <source lang="cpp"> Class CtextBlock { public: ... std::size_t length() const; private: char *pText; mutable std::size_t textLength; // Эти данные-члены всегда могут быть mutable bool lengthIsValid; // модифицированы, даже в константных }; // функциях-членах std::size_t CtextBlock::length() const { if(!lengthIsValid) { textLength = std::strlen(pText); // теперь порядок lengthIsValid = true; // здесь то же } return textLength; } </source> == Как избежать дублирования в константных и неконстантных функциях-членах == Использование mutable – замечательное решение проблемы, когда побитовая константность вас не вполне устраивает, но оно не устраняет всех трудностей, связанных с const. Например, представьте, что operator[] в классе TextBlock (и CTextBlock) не только возвращает ссылку на соответствующий символ, но также проверяет выход за пределы массива, протоколирует информацию о доступе и, возможно, даже проверяет целостность данных. Помещение всей этой логики в обе версии функции operator[] – константную и неконстантную (даже если забыть, что теперь мы имеем необычно длинные встроенные функции – [[Правило 30: Тщательно обдумывайте использование встроенных функций | см. правило 30]]) – приводит к такому вот неуклюжему коду: <source lang="cpp"> class TextBlock { public: ... const char& operator[](std::size_t position) const { ... // выполнить проверку границ массива ... // протоколировать доступ к данным ... // проверить целостность данных return text[position]; } char& operator[](std::size_t position) const { ... // выполнить проверку границ массива ... // протоколировать доступ к данным ... // проверить целостность данных return text[position]; } private: std:string text; }; </source> Ох! Налицо все неприятности, связанные с дублированием кода: увеличение времени компиляции, размера программы и неудобство сопровождения. Конечно, можно переместить весь код для проверки выхода за границы массива и прочего в отдельную функцию-член (естественно, закрытую), которую будут вызывать обе версии operator[], но обращения к этой функции все же будут дублироваться. В действительности было бы желательно реализовать функциональность operator[] один раз, а использовать в двух местах. То есть одна версия operator[] должна вызывать другую. И это подводит нас к вопросу об отбрасывании константности.<br> С самого начала отметим, отбрасывать константность нехорошо. Я посвятил целое [[Правило 27: Не злоупотребляйте приведением типов | правило 27]] тому, чтобы убедить вас не делать этого, но дублирование кода – тоже не сахар. В данном случае константная версия operator[] делает в точности то же самое, что неконстантная, и отличие между ними – лишь в присутствии модификатора const. В этой ситуации отбрасывать const безопасно, поскольку пользователь, вызывающий неконстантный operator[], так или иначе должен получить неконстантный объект. Ведь в противном случае он не стал бы вызывать неконстантную функцию. Поэтому реализация неконстантного operator[] путем вызова константной версии – это безопасный способ избежать дублирования кода, даже пусть даже для этого требуется воспользоваться оператором const_cast. Ниже приведен получающийся в результате код, но он станет яснее после того, как вы прочитаете следующие далее объяснения: <source lang="cpp"> class TextBlock { public: ... const char& operator[](std::size_t position) const // то же, что и раньше { ... ... ... return text[position]; } char& operator[](std::size_t position) const // теперь просто // вызываем const op[] { return const_cast( // из возвращаемого типа // op[] исключить const static_cast(*this) // добавить const типу // *this [position] // вызвать константную ); // версию op[] } ... }; </source> Как видите, код включает два приведения, а не одно. Мы хотим, чтобы неконстантный operator[] вызывал константный, но если внутри неконстантного оператора [] просто вызовем operator[], то получится рекурсивный вызов. Во избежание бесконечной рекурсии нужно указать, что мы хотим вызвать const operator[], но прямого способа сделать это не существует. Поэтому мы приводим *this от типа TextBlock& к const TextBlock&. Да, мы выполняем приведение, чтобы добавить константность! Таким образом, мы имеем два приведения: одно добавляет константность *this (чтобы был вызван const operator[]), а второе – исключает const из типа возвращаемого значения.<br> Приведение, которое добавляет const, выполняет безопасное преобразование (от неконстантного объекта к константному), поэтому мы используем для этой цели static_cast. Приведение же, которое отбрасывает const, может быть выполнено только с помощью const_cast, поэтому у нас здесь нет выбора. (Строго говоря, выбор есть. Приведение в стиле C также работает, но, как я объясняю в [[Правило 27: Не злоупотребляйте приведением типов | правиле 27]], такие приведения редко являются правильным рещением. Если вы не знакомы с операторами static_cast или const_cast, прочитайте о них в [[Правило 27: Не злоупотребляйте приведением типов | правиле 27]].)<br> Помимо всего прочего, в этом примере мы вызываем оператор, поэтому синтаксис выглядит немного странно. Возможно, этот код не займет приз на конкурсе красоты, зато позволяет достичь нужного эффекта – избежать дублирования посредством реализации неконстантной версии operator[] в терминах константной. И хотя для достижения цели пришлось воспользоваться неуклюжим синтаксисом, который сможете понять только вы сами, однако техника реализации неконстантных функций-членов через неконстантные определенно заслуживает того, чтобы ее знать.<br> А еще нужно иметь в виду, что решать эту задачу наоборот – путем вызова неконстантной версии из константной – неправильно. Помните, что константная функция-член обещает никогда не изменять логическое состояние объекта, а неконстантная не дает таких гарантий. Если вы вызовете неконстантную функцию из константной, то рискуете получить ситуацию, когда объект, который не должен модифицироваться, будет изменен. Вот почему этого не следует делать: чтобы объект не изменился. Фактически, чтобы получить компилируемый код, вам пришлось бы использовать const_cast для отбрасывания константности *this, а это явный признак неудачного решения. Обратная последовательность вызовов – такая, как описана выше, – безопасна. Неконстантная функция-член может делать все, что захочет с объектом, поэтому вызов из нее константной функции-члена ничем не грозит. Потому-то мы и применяем к *this оператор static_cast, отбрасывания константности при этом не происходит.<br> Как я уже упоминал в начале этого правила, модификатор const – чудесная вещь. Для указателей и итераторов; для объектов, на которые ссылаются указатели, итераторы и ссылки; для параметров функций и возвращаемых ими значений; для локальных переменных, для функций-членов – всюду const ваш мощный союзник. Используйте его, где только возможно. Вам понравится! 22069a70b14c090447b508942e38fc5efa8147c1 18 17 2013-06-07T12:22:14Z 80.76.185.8 0 wikitext text/x-wiki Замечательное свойство модификатора const состоит в том, что он накладывает определенное семантическое ограничение: данный объект не должен модифицироваться, – и компилятор будет проводить это ограничение в жизнь. const позволяет указать компилятору и программистам, что определенная величина должна оставаться неизменной. Во всех подобных случаях вы должны обозначить это явным образом, призывая себе на помощь компилятор и гарантируя тем самым, что ограничение не будет нарушено.<br> Ключевое слово const удивительно многосторонне. Вне классов вы можете использовать его для определения констант в глобальной области или в пространстве имен ([[см. правило 2]]), а также для статических объектов (внутри файла, функции или блока). Внутри классов допустимо применять его как для статических, так и для нестатических данных-членов. Для указателей можно специфицировать, должен ли быть константным сам указатель, данные, на которые он указывает, либо и то, и другое (или ни то, ни другое): <source lang="cpp"> char greeting[] = “Hello”; char *p = greeting; // неконстантный указатель, // неконстантные данные const char *p = greeting; // неконстантный указатель, // константные данные char * const p = greeting; // константный указатель, // неконстантные данные const char * const p = greeting; // константный указатель, // константные данные </source> Этот синтаксис не так страшен, как может показаться. Если слово const появляется слева от звездочки, константным является то, на что указывает указатель; если справа, то сам указатель является константным. Наконец, если же слово const появляется с обеих сторон, то константно и то, и другое.<br> Когда то, на что указывается, – константа, некоторые программисты ставят const перед идентификатором типа. Другие – после идентификатора типа, но перед звездочкой. Семантической разницы здесь нет, поэтому следующие функции принимают параметр одного и того же типа: <source lang="cpp"> void f1(const Widget *pw); // f1 принимает указатель на // константный объект Widget void f2(Widget const *pw); // то же самое делает f2 </source> Поскольку в реальном коде встречаются обе формы, следует привыкать и к той, и к другой.<br> Итераторы STL смоделированы на основе указателей, поэтому iterator ведет себя почти как указатель T*. Объявление const-итератора подобно объявлению const-указателя (то есть записи T* const): итератор не может начать указывать на что-то другое, но то, на что он указывает, может быть модифицировано. Если вы хотите иметь итератор, который указывал бы на нечто, что запрещено модифицировать (то есть STL-аналог указателя const T*), то вам понадобится константный итератор: <source lang="cpp"> std::vector vec; ... const std::vector::iterator iter = // iter работает как T* const vec.begin(); *iter = 10; // Ok, изменяется то, на что // указывает iter ++iter; // ошибка! iter константный std::vector::const_iterator citer = // citer работает как const T* vec.begin(); *citer = 10; // ошибка! *citer константный ++citer; // нормально, citer изменяется </source> Некоторые из наиболее интересных применений const связаны с объявлениями функций. В этом случае const может относиться к возвращаемому функцией значению, к отдельным параметрам, а для функций-членов – еще и к функции в целом.<br> Если указать в объявлении функции, что она возвращает константное значение, то можно уменьшить количество ошибок в клиентских программах, не снижая уровня безопасности и эффективности. Например, рассмотрим объявление функции operator* для рациональных чисел, введенное в [[правиле 24]]: <source lang="cpp"> class Rational {…} const Rational operator*(const Rational& lhs, const Rational& rhs); </source> Многие программисты удивятся, впервые увидев такое объявление. Почему результат функции operator* должен быть константным объектом? Потому что в противном случае пользователь получил бы возможность делать вещи, которые иначе как надругательством над здравым смыслом не назовешь: <source lang="cpp"> Rational a, b, c; … (a*b)=c; // присваивание произведению a*b! </source> Я не знаю, с какой стати программисту пришло бы в голову присваивать значение произведению двух чисел, но могу точно сказать, что иногда такое может случиться по недосмотру. Достаточно простой опечатки (при условии, что тип может быть преобразован к bool): <source lang="cpp"> if (a*b = c)... // имелось в виду сравнение! </source> Такой код был бы совершенно некорректным, если бы a и b имели встроенный тип. Одним из критериев качества пользовательских типов является совместимость со встроенными (см. также правило 18), а возможность присваивания значения результату произведения двух объектов представляется мне весьма далекой от совместимости. Если же объявить, что operator* возвращает константное значение, то такая ситуация станет невозможной. Вот почему Так Следует Поступать.<br> В отношении аргументов с модификатором const трудно сказать что-то новое; они ведут себя как локальные константные const-объекты. Всюду, где возможно, добавляйте этот модификатор. Если модифицировать аргумент или локальный объект нет необходимости, объявите его как const. Вам всего-то придется набрать шесть символов, зато это предотвратит досадные ошибки типа «хотел напечатать ==, а нечаянно напечатал =» (к чему это приводит, мы только что видели). == Константные функции-члены == Назначение модификатора const в объявлении функций-членов – определить, какие из них можно вызывать для константных объектов. Такие функции-члены важны по двум причинам. Во-первых, они облегчают понимание интерфейса класса, ведь полезно сразу видеть, какие функции могут модифицировать объект, а какие нет. Во-вторых, они обеспечивают возможность работать с константными объектами. Это очень важно для написания эффективного кода, потому что, как объясняется в [[Правило 20: Предпочитайте передачу по ссылке на const передаче по значению | правиле 20]], один из основных способов повысить производительность программ на C++ – передавать объекты по ссылке на константу. Но эта техника будет работать только в случае, когда функции-члены для манипулирования константными объектами объявлены с модификатором const.<br> Многие упускают из виду, что функции, отличающиеся только наличием const в объявлении, могут быть перегружены. Это, однако, важное свойство C++. Рассмотрим класс, представляющий блок текста: <source lang="cpp"> class TextBlock { public: ... const char& operator[](std::size_t position) const // operator[] для {return text[position];} // константных объектов char& operator[](std::size_t position) // operator[] для {return text[position];} // неконстантных объектов private: std::string text; }; </source> Функцию operator[] в классе TextBlock можно использовать следующим образом: <source lang="cpp"> TextBlock tb(“Hello”); Std::cout << tb[0]; // вызов неконстантного // оператора TextBlock::operator[] const TextBlock ctb(“World”); Std::cout << ctb[0]; // вызов константного // оператора TextBlock::operator[] </source> Кстати, константные объекты чаще всего встречаются в реальных программах в результате передачи по указателю или ссылке на константу. Приведенный выше пример ctb является довольно искусственным. Но вот вам более реалистичный: <source lang="cpp"> void print(const TextBlock& ctb) // в этой функции ctb – ссылка // на константный объект { std::cout << ctb[0]; // вызов const TextBlock::operator[] ... } </source> Перегружая operator[] и создавая различные версии с разными возвращаемыми типами, вы можете по-разному обрабатывать константные и неконстантные объекты TextBlock: <source lang="cpp"> std::cout << tb[0]; // нормально – читается // неконстантный TextBlock tb[0] = ‘x’; // нормально – пишется // неконстантный TextBlock std::cout << ctb[0]; // нормально – читается // константный TextBlock ctb[0] = ‘x’; // ошибка! – запись // константного TextBlock </source> Отметим, что ошибка здесь связана только с типом значения, возвращаемого operator[]; сам вызов operator[] проходит нормально. Причина ошибки – в попытке присвоить значение объекту типа const char&, потому что это именно такой тип возвращается константной версией operator[]. Отметим также, что тип, возвращаемый неконстантной версией operator[], – это ссылка на char, а не сам char. Если бы operator[] возвращал просто char, то следующее предложение не скомпилировалось бы: <source lang="cpp"> tb[0] = ‘x’; </source> Это объясняется тем, что возвращаемое функцией значение встроенного типа модифицировать некорректно. Даже если бы это было допустимо, тот факт, что C++ возвращает объекты по значению ([[Правило 20: Предпочитайте передачу по ссылке на const передаче по значению | см. правило 20]]), означал бы следующее: модифицировалась копия tb.text[0], а не само значение tb.text[0]. Вряд ли это то, чего вы ожидаете.<br> Давайте немного передохнем и пофилософствуем. Что означает для функции-члена быть константной? Существует два широко распространенных понятия: побитовая константность (также известная как физическая константность) и логическая константность.<br> Сторонники побитовой константности полагают, что функция-член константна тогда и только тогда, когда она не модифицирует никакие данные-члены объекта (за исключением статических), то есть не модифицирует ни одного бита внутри объекта. Определение побитовой константности хорошо тем, что ее нарушение легко обнаружить: компилятор просто ищет присваивания членам класса. Фактически, побитовая константность – это константность, определенная в C++: функция-член с модификатором const не может модифицировать нестатические данные-члены объекта, для которого она вызвана. К сожалению, многие функции-члены, которые ведут себя далеко не константно, проходят побитовый тест. В частности, функция-член, которая модифицирует то, на что указывает указатель, часто не ведет себя как константная. Но если объекту принадлежит только указатель, то функция формально является побитово константной, и компилятор не станет возражать. Это может привести к неожиданному поведению. Например, предположим, что есть класс подобный Text-Block, где данные хранятся в строках типа char * вместо string, поскольку это необходимо для передачи в функции, написанные на языке C, который не понимает, что такое объекты типа string. <source lang="cpp"> class CtextBlock { public: ... char& operator[](std::size_t position) const // неудачное (но побитово { return pText[position]} // константное) // объявление operator[] private: char *pText; }; </source> В этом классе функция operator[] (неправильно!) объявлена как константная функция-член, хотя она возвращает ссылку на внутренние данные объекта (эта тема обсуждается [[Правило 28: Избегайте возвращения «дескрипторов» внутренних данных | в правиле 28]]). Оставим это пока в стороне и отметим, что реализация operator[] никак не модифицирует pText. В результате компилятор спокойно сгенерирует код для функции operator[]. Ведь она действительно является побитово константной, а это все, что компилятор может проверить. Но посмотрите, что происходит: <source lang="cpp"> const CtextBlock cctb(“Hello”); // объявление константного объекта char &pc = &cctb[0]; // вызов const operator[] для получения // указателя на данные cctb *pc = ‘j’; // cctb теперь имеет значение “Jello” </source> Несомненно, есть что-то некорректное в том, что вы создаете константный объект с определенным значением, вызываете для него только константную функцию-член и тем не менее изменяете его значение!<br> Это приводит нас к понятию логической константности. Сторонники этой философии утверждают, что функции-члены с const могут модифицировать некоторые биты вызвавшего их объекта, но только так, чтобы пользователь не мог этого обнаружить. Например, ваш класс CTextBlock мог бы кэшировать длину текстового блока при каждом запросе: <source lang="cpp"> Class CtextBlock { public: ... std::size_t length() const; private: char *pText; std::size_t textLength; // последнее вычисленное значение длины // текстового блока bool lengthIsValid; // корректна ли длина в данный момент }; std::size_t CtextBlock::length() const { if(!lengthIsValid) { textLength = std::strlen(pText); // ошибка! Нельзя присваивать lengthIsValid = true; // значение textLength и } // lengthIsValid в константной // функции-члене return textLength; } </source> Эта реализация length(), конечно же, не является побитово константной, поскольку может модифицировать значения членов textLength и lengthlsValid. Но в то же время со стороны кажется, что константности объектов CTextBlock это не угрожает. Однако компилятор не согласен. Он настаивает на побитовой константности. Что делать? Решение простое: используйте модификатор mutable. Он освобождает нестатические данные-члены от ограничений побитовой константности: <source lang="cpp"> Class CtextBlock { public: ... std::size_t length() const; private: char *pText; mutable std::size_t textLength; // Эти данные-члены всегда могут быть mutable bool lengthIsValid; // модифицированы, даже в константных }; // функциях-членах std::size_t CtextBlock::length() const { if(!lengthIsValid) { textLength = std::strlen(pText); // теперь порядок lengthIsValid = true; // здесь то же } return textLength; } </source> == Как избежать дублирования в константных и неконстантных функциях-членах == Использование mutable – замечательное решение проблемы, когда побитовая константность вас не вполне устраивает, но оно не устраняет всех трудностей, связанных с const. Например, представьте, что operator[] в классе TextBlock (и CTextBlock) не только возвращает ссылку на соответствующий символ, но также проверяет выход за пределы массива, протоколирует информацию о доступе и, возможно, даже проверяет целостность данных. Помещение всей этой логики в обе версии функции operator[] – константную и неконстантную (даже если забыть, что теперь мы имеем необычно длинные встроенные функции – [[Правило 30: Тщательно обдумывайте использование встроенных функций | см. правило 30]]) – приводит к такому вот неуклюжему коду: <source lang="cpp"> class TextBlock { public: ... const char& operator[](std::size_t position) const { ... // выполнить проверку границ массива ... // протоколировать доступ к данным ... // проверить целостность данных return text[position]; } char& operator[](std::size_t position) const { ... // выполнить проверку границ массива ... // протоколировать доступ к данным ... // проверить целостность данных return text[position]; } private: std:string text; }; </source> Ох! Налицо все неприятности, связанные с дублированием кода: увеличение времени компиляции, размера программы и неудобство сопровождения. Конечно, можно переместить весь код для проверки выхода за границы массива и прочего в отдельную функцию-член (естественно, закрытую), которую будут вызывать обе версии operator[], но обращения к этой функции все же будут дублироваться. В действительности было бы желательно реализовать функциональность operator[] один раз, а использовать в двух местах. То есть одна версия operator[] должна вызывать другую. И это подводит нас к вопросу об отбрасывании константности.<br> С самого начала отметим, отбрасывать константность нехорошо. Я посвятил целое [[Правило 27: Не злоупотребляйте приведением типов | правило 27]] тому, чтобы убедить вас не делать этого, но дублирование кода – тоже не сахар. В данном случае константная версия operator[] делает в точности то же самое, что неконстантная, и отличие между ними – лишь в присутствии модификатора const. В этой ситуации отбрасывать const безопасно, поскольку пользователь, вызывающий неконстантный operator[], так или иначе должен получить неконстантный объект. Ведь в противном случае он не стал бы вызывать неконстантную функцию. Поэтому реализация неконстантного operator[] путем вызова константной версии – это безопасный способ избежать дублирования кода, даже пусть даже для этого требуется воспользоваться оператором const_cast. Ниже приведен получающийся в результате код, но он станет яснее после того, как вы прочитаете следующие далее объяснения: <source lang="cpp"> class TextBlock { public: ... const char& operator[](std::size_t position) const // то же, что и раньше { ... ... ... return text[position]; } char& operator[](std::size_t position) const // теперь просто // вызываем const op[] { return const_cast( // из возвращаемого типа // op[] исключить const static_cast(*this) // добавить const типу // *this [position] // вызвать константную ); // версию op[] } ... }; </source> Как видите, код включает два приведения, а не одно. Мы хотим, чтобы неконстантный operator[] вызывал константный, но если внутри неконстантного оператора [] просто вызовем operator[], то получится рекурсивный вызов. Во избежание бесконечной рекурсии нужно указать, что мы хотим вызвать const operator[], но прямого способа сделать это не существует. Поэтому мы приводим *this от типа TextBlock& к const TextBlock&. Да, мы выполняем приведение, чтобы добавить константность! Таким образом, мы имеем два приведения: одно добавляет константность *this (чтобы был вызван const operator[]), а второе – исключает const из типа возвращаемого значения.<br> Приведение, которое добавляет const, выполняет безопасное преобразование (от неконстантного объекта к константному), поэтому мы используем для этой цели static_cast. Приведение же, которое отбрасывает const, может быть выполнено только с помощью const_cast, поэтому у нас здесь нет выбора. (Строго говоря, выбор есть. Приведение в стиле C также работает, но, как я объясняю в [[Правило 27: Не злоупотребляйте приведением типов | правиле 27]], такие приведения редко являются правильным рещением. Если вы не знакомы с операторами static_cast или const_cast, прочитайте о них в [[Правило 27: Не злоупотребляйте приведением типов | правиле 27]].)<br> Помимо всего прочего, в этом примере мы вызываем оператор, поэтому синтаксис выглядит немного странно. Возможно, этот код не займет приз на конкурсе красоты, зато позволяет достичь нужного эффекта – избежать дублирования посредством реализации неконстантной версии operator[] в терминах константной. И хотя для достижения цели пришлось воспользоваться неуклюжим синтаксисом, который сможете понять только вы сами, однако техника реализации неконстантных функций-членов через неконстантные определенно заслуживает того, чтобы ее знать.<br> А еще нужно иметь в виду, что решать эту задачу наоборот – путем вызова неконстантной версии из константной – неправильно. Помните, что константная функция-член обещает никогда не изменять логическое состояние объекта, а неконстантная не дает таких гарантий. Если вы вызовете неконстантную функцию из константной, то рискуете получить ситуацию, когда объект, который не должен модифицироваться, будет изменен. Вот почему этого не следует делать: чтобы объект не изменился. Фактически, чтобы получить компилируемый код, вам пришлось бы использовать const_cast для отбрасывания константности *this, а это явный признак неудачного решения. Обратная последовательность вызовов – такая, как описана выше, – безопасна. Неконстантная функция-член может делать все, что захочет с объектом, поэтому вызов из нее константной функции-члена ничем не грозит. Потому-то мы и применяем к *this оператор static_cast, отбрасывания константности при этом не происходит.<br> Как я уже упоминал в начале этого правила, модификатор const – чудесная вещь. Для указателей и итераторов; для объектов, на которые ссылаются указатели, итераторы и ссылки; для параметров функций и возвращаемых ими значений; для локальных переменных, для функций-членов – всюду const ваш мощный союзник. Используйте его, где только возможно. Вам понравится! == Что следует помнить == *Объявление чего-либо с модификатором const помогает компиляторам обнаруживать ошибки. const можно использовать с объектами в любой области действия, с параметрами функций и возвращаемых значений, а также с функциями-членами в целом. *Компиляторы проверяют побитовую константность, но вы должны программировать, применяя логическую константность. *Когда константные и неконстантные функции-члены имеют, по сути, одинаковую реализацию, то дублирования кода можно избежать, заставив неконстантную версию вызывать константную. 47fd364df57b12e76d0cabf07bf9048fbb862206 Правило 41: Разберитесь в том, что такое неявные интерфейсы и полиморфизм на этапе компиляции 0 6 15 2013-06-06T06:56:30Z 80.76.185.8 0 Новая страница: «49» wikitext text/x-wiki 49 2e01e17467891f7c933dbaa00e1459d23db3fe4f Правило 4: Прежде чем использовать объекты, убедитесь, что они инициализированы 0 7 20 2013-06-07T13:21:59Z 80.76.185.8 0 Новая страница: «<P>Отношение C++ к инициализации значений объектов может показаться странным. Например, ес…» wikitext text/x-wiki <P>Отношение C++ к инициализации значений объектов может показаться странным. Например, если вы пишете:</P> <source lang="cpp"> int x; </source> <P>то в некоторых контекстах переменная x будет гарантированно инициализирована нулем, а в других – нет. Если вы пишете:</P> <source lang="cpp"> class Point { int x, y; }; ... Point p; </source> <P>то члены-данные объекта p иногда будут инициализированы (нулями), а иногда – нет. Если вы перешли к C++ от языка, где неинициализированные объекты не могут существовать, обратите на это внимание.</P> <P>Чтение неинициализированных значений может быть причиной неопределенного поведения. На некоторых платформах такое простое действие, как доступ к неинициированному значению для чтения, может вызвать аварийную остановку программы. Но чаще вы получите случайный набор битов, который испортит внутреннее состояние объекта, в который они записываются, и в конечном итоге это приведет к необъяснимому поведению программы и длительному поиску ошибки в отладчике.</P> <P>Сформулируем правила, которые описывают, когда инициализация объекта гарантируется, а когда нет. К сожалению, эти правила достаточно сложны – на мой взгляд, слишком сложны, чтобы их стоило запоминать. Вообще, если вы работаете с C-частью C++ (см. правило 1) и инициализация может стоить определенных затрат во время исполнения, то не гарантируется, что она произойдет. Это объясняет, почему содержимое массивов (в C-части C++) не обязательно инициализируется, а содержимое вектора (из STL-части C++) инициализируется всегда.</P> <P>По-видимому, лучший способ поведения в такой неопределенной ситуации – <EM>всегда</EM> инициализировать объекты, прежде чем их использовать. Для объектов встроенных типов, не являющихся членами классов, это нужно делать вручную. Например:</P> <source lang="cpp"> int x = 0; // ручная инициализация int const char * text = “Строка в стиле C”; // ручная инициализация указателя // (см. также правило 3) double d; // «инициализация» чтением std::cin >> d; // из входного потока </source> <P>Почти во всех остальных случаях ответственность за инициализацию ложится на конструкторы. Правило простое: убедитесь, что все конструкторы инициализируют в объекте всё.</P> <P>Этому правилу легко следовать, но важно не путать присваивание с инициализацией. Рассмотрим конструктор класса, представляющего записи в адресной книге:</P> <source lang="cpp"> class PhoneNumber {…} class ABEntry { // ABEntry = “Address Book Entry” public: ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones); private: std::string theName; std::string theAddress; std::list<PhoneNumber> thePhones; int numTimesConsulted; }; ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) { theName = name; // все это присваивание, а не инициализация theAddress = address; thePhones = phones; numTimesConsulted = 0; } </source> <P>Да, в результате порождаются объекты ABEntry со значениями, которых вы ожидаете, но это все же не лучший подход. Правила C++ оговаривают, что члены объекта инициируются <EM>перед</EM> входом в тело конструктора. То есть внутри конструктора ABEntry члены theName, theAddress и thePhones не инициализируются, а им <EM>присваиваются</EM> значения. Инициализация происходит ранее: когда автоматически вызываются их конструкторы перед входом в тело конструктора ABEntry. Это не касается numTimesConsulted, поскольку этот член относится к встроенному типу. Для него нет никаких гарантий того, что он вообще будет инициализирован перед присваиванием.</P> <P>Лучший способ написания конструктора ABEntry – использовать список инициализации членов вместо присваивания:</P> <source lang="cpp"> ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) :theName(name), // теперь это все – инициализации theAddress(address), thePhones(phones), numTimesConsulted(0) {} // тело конструктора теперь пусто </source> <P>Этот конструктор дает тот же самый конечный результат, что и предыдущий, но часто оказывается более эффективным. Версия, основанная на присваиваниях, сначала вызывает конструкторы по умолчанию для инициализации theName, theAddress и thePhones, а затем сразу присваивает им новые значения, затирая те, что уже были присвоены в конструкторах по умолчанию. Таким образом, вся работа конструкторов по умолчанию тратится впустую. Подход со списком инициализации членов позволяет избежать этой проблемы, поскольку аргументы в списке инициализации используются в качестве аргументов конструкторов для различных членов-данных. В этом случае theName создается конструктором копирования из name, theAddress – из address, thePhones – из phones. Для большинства типов единственный вызов конструктора копирования более эффективен – иногда <EM>намного</EM> более эффективен, чем вызов конструкторов по умолчанию с последующим вызовом операторов присваивания.</P> <P>Для объектов встроенных типов вроде numTimesConsulted нет разницы по затратам между инициализацией и присваиванием, но для единообразия часто лучше инициировать все посредством списка инициализации членов. Такие списки можно применять даже тогда, когда данные-члены инициализируются конструкторами по умолчанию: просто не передавайте никаких аргументов соответствующему конструктору. Например, если у ABEntry есть конструктор, не принимающий параметров, то он может быть реализован примерно так:</P> <source lang="cpp"> ABEntry() :theName(), // вызвать конструктор по умолчанию для theName theAddress(), // сделать то же для theAddress и для thePhones; thePhones(), // но явно инициализировать нулем numTimesConsulted numTimesConsulted(0) {} </source> <P>Поскольку компилятор автоматически вызывает конструкторы по умолчанию для данных-членов пользовательских типов, когда для них отсутствуют инициализаторы в списке инициализации членов, некоторые программисты считают приведенный выше код избыточным. Это понятно, но, придерживаясь политики всегда перечислять все данные-члены в списках инициализации, вы избавляете себя от необходимости помнить, какие члены будут инициализированы, если их пропустить, а какие – нет. Например, поскольку numTimesConsulted относится к встроенному типу, то исключение его из списка инициализации может открыть двери неопределенному поведению.</P> <P>Иногда список инициализации просто <EM>необходимо</EM> использовать, даже для встроенных типов. Например, данные-члены, которые являются константами либо ссылками, обязаны быть инициализированы, так как они не могут получить значения посредством присваивания (см. также правило 5). Чтобы избежать необходимости помнить, когда данные-члены должны быть инициализированы в списке инициализации, а когда это не обязательно, проще делать это <EM>всегда.</EM> Иногда это обязательно, а часто – более эффективно, чем присваивание.</P> <P>Во многих классах есть несколько конструкторов, и каждый конструктор имеет свой собственный список инициализации. Если у класса много данных-членов или базовых классов, то наличие большого числа списков инициализации порождает нежелательное дублирование кода (в списках) и тоску (у программистов). В таких случаях имеет смысл опустить в списках инициализации те данные-члены, для которых присваивание работает так же, как настоящая инициализация, переместив инициализацию в одну (обычно закрытую) функцию, которую вызывают все конструкторы. Этот подход может быть особенно полезен, если начальные значения должны быть загружены из файла или базы данных. Однако, вообще говоря, инициализация членов посредством списков инициализации более предпочтительна, чем псевдоинициализация присваиванием.</P> <P>Один из аспектов C++, на который можно положиться, – это порядок, в котором инициализируются данные объектов. Этот порядок всегда один и тот же: базовые классы инициализируются раньше производных (см. также правило 12), а внутри класса члены-данные инициализируются в том порядке, в котором объявлены. Например, в классе ABEntry член theName всегда будет инициализирован первым, theAddress – вторым, thePhones – третьим, а numTimesConsulted – последним. Это верно даже в случае, если в списке инициализации членов они перечислены в другом порядке (что, к сожалению, не запрещено). Чтобы не вводить в заблуждение человека, читающего вашу программу, и во избежание ошибок непонятного происхождения, всегда перечисляйте данные-члены в списке инициализации в том порядке, в котором они объявлены в классе.</P> <P>Позаботившись о явной инициализации объектов встроенных типов, которые не являются членами классов, и обеспечив правильную инициализацию базовых классов и их данных-членов посредством списков инициализации, у вас останется только одна вещь, о чем нужно будет подумать. Речь идет о порядке инициализации нелокальных статических объектов, объявленных в разных единицах трансляции.</P> <P>Отнесемся к этой фразе со всем вниманием.</P> <P><EM>Статический объект</EM> существует от момента, когда был сконструирован, и до конца работы программы. Объекты, размещенные в стеке и в «куче», к статическим не относятся. Статическими являются глобальные объекты, объекты, объявленные в области действия пространства имен, объекты, объявленные с ключевым словом static внутри классов и функций, а также в области действия отдельного файла с исходным текстом. Статические объекты, объявленные внутри функций, известны как <EM>локальные статические объекты</EM> (поскольку они локальны по отношению к функции), а все прочие называют <EM>нелокальными статическими объектами.</EM> Статические объекты автоматически уничтожаются при завершении программы, то есть при выходе из функции main() автоматически вызываются их деструкторы.</P> <P><EM>Единица трансляции (translation unit)</EM> – это исходный код, который порождает отдельный объектный файл. Обычно это один исходный файл плюс все файлы, включенные в него директивой #include.</P> <P>Проблема возникает, когда есть, по крайней мере, два отдельно компилируемых исходных файла, каждый из которых содержит, по крайней мере, один нелокальный статический объект (то есть глобальный объект либо объявленный в области действия пространства имен, класса или файла). Суть ее в том, что если инициализация нелокального статического объекта происходит в одной единице трансляции, а используется он в другой, то такой объект может оказаться неинициализированным в момент использования, поскольку <EM>относительный порядок инициализации нестатических локальных объектов, определенных в разных единицах трансляции, не определен.</EM></P> <P>Рассмотрим пример. Предположим, у вас есть класс FileSystem, который делает файлы из Internet неотличимыми от локальных. Поскольку ваш класс представляет мир как единую файловую систему, вы могли бы создать в глобальной области действия или в пространстве имен соответствующий ей специальный объект:</P> <source lang="cpp"> class FileSystem { // из вашей библиотеки public: ... std::size_t numDisks() const; // одна из многих функций-членов ... }; extern FileSystem tfs; // объект для использования клиентами // “tfs” = “the file system” </source> <P>Класс FileSystem определенно не тривиален, поэтому использование объекта theFileSystem до того, как он будет сконструирован, приведет к катастрофическим последствиям.</P> <P>Теперь предположим, что некий пользователь создает класс, описывающий каталоги файловой системы. Естественно, его класс будет использовать объект theFileSystem:</P> <source lang="cpp"> class Directory { // создан пользователем public: Directory( params ); ... }; Directory::Directory( params ) { ... std::size_t disks = tfs.numDisks(); // использование объекта tfs ... } </source> <P>Далее предположим, что пользователь решает создать отдельный глобальный объект класса Directory, представляющий каталог для временных файлов:</P> <source lang="cpp"> Directory tempDir( params ); // каталог для временных файлов </source> <P>Теперь проблема порядка инициализации становится очевидной: если объект tfs не инициализирован раньше, чем tempDir, то конструктор tempDir попытается использовать tfs до его инициализации. Но tfs и tempDir были созданы разными людьми в разное время и находятся в разных исходных файлах – это нелокальные статические объекты, определенные в разных единицах трансляции. Как вы можете быть уверены, что tfs будет инициализирован раньше, чем tempDir?</P> <P>Да никак! Еще раз повторю: <EM>относительный порядок инициализации нестатических локальных объектов, определенных в разных единицах трансляции, не определен.</EM> На то есть своя причина. Определить «правильный» порядок инициализации нелокальных статических объектов трудно. Очень трудно. Неразрешимо трудно. В наиболее общем случае – при наличии многих единиц трансляции и нелокальных статических объектов, сгенерированных путем неявной конкретизации шаблонов (которые и сами могут быть результатом неявной конкретизации других шаблонов) – не только невозможно определить правильный порядок инициализации, но обычно даже не стоит искать частные случаи, когда этот порядок в принципе определить можно.</P> <P>К счастью, небольшое изменение в проекте программы позволяет полностью устранить эту проблему. Нужно лишь переместить каждый нелокальный статический объект в отдельную функцию, в которой он будет объявлен статическим. Эти функции возвращают ссылки на объекты, которые в них содержатся. Клиенты затем вызывают функции вместо непосредственного обращения к объектам. Другими словами, нелокальные статические объекты заменяются <EM>локальными</EM> статическими объектами (знакомые с паттернами проектирования легко узнают в этом описании типичную реализацию паттерна Singleton).</P> <P>Этот подход основан на том, что C++ гарантирует: локальные статические объекты инициализируются в первый раз, когда определение объекта встречается при вызове этой функции. Поэтому если вы замените прямой доступ к нелокальным статическим объектам вызовом функций, возвращающих ссылки на расположенные внутри них локальные статические объекты, то можете быть уверены, что ссылки, возвращаемые из функций, будут ссылаться на инициализированные объекты. Дополнительное преимущество заключается в том, что если вы никогда не вызываете функцию, эмулирующую нелокальный статический объект, то и не придется платить за создание и уничтожение объекта, чего не скажешь о реальных нелокальных статических объектах.</P> <P>Вот как этот прием применяется к объектам tfs и tempDir:</P> <source lang="cpp"> class FileSystem {...}; // как раньше FileSystem& tfs() // эта функция заменяет объект tfs, она может { // быть статической в классе FileSystem static FileSystem fs; // определение и инициализация локального // статического объекта return fs; // возврат ссылки на него } class Directory {...}; // как раньше Directory::Directory( params ) // как раньше, но вместо ссылки на tfs { // вызов tfs() ... std::size_t disks = tfs().numDisks(); ... } Directory& tempDir() // эта функция заменяет объект tempDir, { // может быть статической в классе Directory static Directory td; // определение/инициализация локального // статического объекта return td; // возврат ссылки на него } </source> <P>Клиенты работают с этой модифицированной программой так же, как раньше, за исключением того, что вместо tfs и tempDir они теперь обращаются к tfs() и tempDir(). Иными словами, используют ссылки на объекты, возвращенные функциями, вместо использования самих объектов.</P> <P>Функции, которые в соответствии с данной схемой возвращают ссылки, всегда просты: определить и инициализировать локальный статический объект в строке 1 и вернуть его в строке 2. В связи с этим у вас может возникнуть искушение объявить их встроенными, особенно, если они часто вызываются (см. правило 30). С другой стороны, тот факт, что эти функции содержат в себе статические объекты, усложняет их применение в многопоточных системах. Но тут никуда не деться: неконстантные статические объекты любого рода – локальные или нелокальные – представляют проблему в случае наличия в программе нескольких потоков. Решить ее можно, например, вызвав самостоятельно все функции, возвращающие ссылки, на этапе запуска программы, когда еще работает только один поток. Это исключит неопределенность в ходе инициализации.</P> <P>Конечно, применимость идеи функций, возвращающих ссылки, для предотвращения проблем, связанных с порядком инициализации, зависит от того, существует ли в принципе разумный порядок инициализации ваших объектов. Если вы напишете код, в котором объект A должен быть инициализирован прежде, чем объект B, и одновременно сделаете инициализацию A зависимой от инициализации B, то вас ждут проблемы – и поделом! Если, однако, вы будете избегать таких патологических ситуаций, то описанная схема сослужит вам добрую службу, по крайней мере, в однопоточных приложениях.</P> <P>Таким образом, чтобы избежать использования объектов до их инициализации, вам следует сделать три вещи. Первое: вручную инициализировать не являющиеся членами объекты встроенных типов. Второе: использовать списки инициализации членов для всех частей объекта. И наконец, третье: обойти за счет правильного проектирования проблему негарантированного порядка инициализации нелокальных статических объектов, определенных в разных единицах трансляции.</P> == Что следует помнить == *Всегда вручную инициализировать объекты встроенных типов, поскольку C++ делает это, только не всегда. *В конструкторе отдавать предпочтение применению списков инициализации членов перед прямым присваиванием значений в теле конструктора. Перечисляйте данные-члены в списке инициализации в том же порядке, в каком они объявлены в классе. *Избегайте проблем с порядком инициализации в разных единицах трансляции, заменяя нелокальные статические объекты локальными статическими объектами. 643c8932e6c28710e4dea38ef42acbf8bf0a8252 21 20 2013-06-07T13:25:00Z 80.76.185.8 0 wikitext text/x-wiki <P>Отношение C++ к инициализации значений объектов может показаться странным. Например, если вы пишете:</P> <source lang="cpp"> int x; </source> <P>то в некоторых контекстах переменная x будет гарантированно инициализирована нулем, а в других – нет. Если вы пишете:</P> <source lang="cpp"> class Point { int x, y; }; ... Point p; </source> <P>то члены-данные объекта p иногда будут инициализированы (нулями), а иногда – нет. Если вы перешли к C++ от языка, где неинициализированные объекты не могут существовать, обратите на это внимание.</P> <P>Чтение неинициализированных значений может быть причиной неопределенного поведения. На некоторых платформах такое простое действие, как доступ к неинициированному значению для чтения, может вызвать аварийную остановку программы. Но чаще вы получите случайный набор битов, который испортит внутреннее состояние объекта, в который они записываются, и в конечном итоге это приведет к необъяснимому поведению программы и длительному поиску ошибки в отладчике.</P> <P>Сформулируем правила, которые описывают, когда инициализация объекта гарантируется, а когда нет. К сожалению, эти правила достаточно сложны – на мой взгляд, слишком сложны, чтобы их стоило запоминать. Вообще, если вы работаете с C-частью C++ ([[Правило 1: Относитесь к C++ как к конгломерату языков | см. правило 1]]) и инициализация может стоить определенных затрат во время исполнения, то не гарантируется, что она произойдет. Это объясняет, почему содержимое массивов (в C-части C++) не обязательно инициализируется, а содержимое вектора (из STL-части C++) инициализируется всегда.</P> <P>По-видимому, лучший способ поведения в такой неопределенной ситуации – <EM>всегда</EM> инициализировать объекты, прежде чем их использовать. Для объектов встроенных типов, не являющихся членами классов, это нужно делать вручную. Например:</P> <source lang="cpp"> int x = 0; // ручная инициализация int const char * text = “Строка в стиле C”; // ручная инициализация указателя // (см. также правило 3) double d; // «инициализация» чтением std::cin >> d; // из входного потока </source> <P>Почти во всех остальных случаях ответственность за инициализацию ложится на конструкторы. Правило простое: убедитесь, что все конструкторы инициализируют в объекте всё.</P> <P>Этому правилу легко следовать, но важно не путать присваивание с инициализацией. Рассмотрим конструктор класса, представляющего записи в адресной книге:</P> <source lang="cpp"> class PhoneNumber {…} class ABEntry { // ABEntry = “Address Book Entry” public: ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones); private: std::string theName; std::string theAddress; std::list<PhoneNumber> thePhones; int numTimesConsulted; }; ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) { theName = name; // все это присваивание, а не инициализация theAddress = address; thePhones = phones; numTimesConsulted = 0; } </source> <P>Да, в результате порождаются объекты ABEntry со значениями, которых вы ожидаете, но это все же не лучший подход. Правила C++ оговаривают, что члены объекта инициируются <EM>перед</EM> входом в тело конструктора. То есть внутри конструктора ABEntry члены theName, theAddress и thePhones не инициализируются, а им <EM>присваиваются</EM> значения. Инициализация происходит ранее: когда автоматически вызываются их конструкторы перед входом в тело конструктора ABEntry. Это не касается numTimesConsulted, поскольку этот член относится к встроенному типу. Для него нет никаких гарантий того, что он вообще будет инициализирован перед присваиванием.</P> <P>Лучший способ написания конструктора ABEntry – использовать список инициализации членов вместо присваивания:</P> <source lang="cpp"> ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) :theName(name), // теперь это все – инициализации theAddress(address), thePhones(phones), numTimesConsulted(0) {} // тело конструктора теперь пусто </source> <P>Этот конструктор дает тот же самый конечный результат, что и предыдущий, но часто оказывается более эффективным. Версия, основанная на присваиваниях, сначала вызывает конструкторы по умолчанию для инициализации theName, theAddress и thePhones, а затем сразу присваивает им новые значения, затирая те, что уже были присвоены в конструкторах по умолчанию. Таким образом, вся работа конструкторов по умолчанию тратится впустую. Подход со списком инициализации членов позволяет избежать этой проблемы, поскольку аргументы в списке инициализации используются в качестве аргументов конструкторов для различных членов-данных. В этом случае theName создается конструктором копирования из name, theAddress – из address, thePhones – из phones. Для большинства типов единственный вызов конструктора копирования более эффективен – иногда <EM>намного</EM> более эффективен, чем вызов конструкторов по умолчанию с последующим вызовом операторов присваивания.</P> <P>Для объектов встроенных типов вроде numTimesConsulted нет разницы по затратам между инициализацией и присваиванием, но для единообразия часто лучше инициировать все посредством списка инициализации членов. Такие списки можно применять даже тогда, когда данные-члены инициализируются конструкторами по умолчанию: просто не передавайте никаких аргументов соответствующему конструктору. Например, если у ABEntry есть конструктор, не принимающий параметров, то он может быть реализован примерно так:</P> <source lang="cpp"> ABEntry() :theName(), // вызвать конструктор по умолчанию для theName theAddress(), // сделать то же для theAddress и для thePhones; thePhones(), // но явно инициализировать нулем numTimesConsulted numTimesConsulted(0) {} </source> <P>Поскольку компилятор автоматически вызывает конструкторы по умолчанию для данных-членов пользовательских типов, когда для них отсутствуют инициализаторы в списке инициализации членов, некоторые программисты считают приведенный выше код избыточным. Это понятно, но, придерживаясь политики всегда перечислять все данные-члены в списках инициализации, вы избавляете себя от необходимости помнить, какие члены будут инициализированы, если их пропустить, а какие – нет. Например, поскольку numTimesConsulted относится к встроенному типу, то исключение его из списка инициализации может открыть двери неопределенному поведению.</P> <P>Иногда список инициализации просто <EM>необходимо</EM> использовать, даже для встроенных типов. Например, данные-члены, которые являются константами либо ссылками, обязаны быть инициализированы, так как они не могут получить значения посредством присваивания ([[Правило 5: Какие функции C++ создает и вызывает молча | см. также правило 5]]). Чтобы избежать необходимости помнить, когда данные-члены должны быть инициализированы в списке инициализации, а когда это не обязательно, проще делать это <EM>всегда.</EM> Иногда это обязательно, а часто – более эффективно, чем присваивание.</P> <P>Во многих классах есть несколько конструкторов, и каждый конструктор имеет свой собственный список инициализации. Если у класса много данных-членов или базовых классов, то наличие большого числа списков инициализации порождает нежелательное дублирование кода (в списках) и тоску (у программистов). В таких случаях имеет смысл опустить в списках инициализации те данные-члены, для которых присваивание работает так же, как настоящая инициализация, переместив инициализацию в одну (обычно закрытую) функцию, которую вызывают все конструкторы. Этот подход может быть особенно полезен, если начальные значения должны быть загружены из файла или базы данных. Однако, вообще говоря, инициализация членов посредством списков инициализации более предпочтительна, чем псевдоинициализация присваиванием.</P> <P>Один из аспектов C++, на который можно положиться, – это порядок, в котором инициализируются данные объектов. Этот порядок всегда один и тот же: базовые классы инициализируются раньше производных ([[Правило 12: Копируйте все части объекта | см. также правило 12]]), а внутри класса члены-данные инициализируются в том порядке, в котором объявлены. Например, в классе ABEntry член theName всегда будет инициализирован первым, theAddress – вторым, thePhones – третьим, а numTimesConsulted – последним. Это верно даже в случае, если в списке инициализации членов они перечислены в другом порядке (что, к сожалению, не запрещено). Чтобы не вводить в заблуждение человека, читающего вашу программу, и во избежание ошибок непонятного происхождения, всегда перечисляйте данные-члены в списке инициализации в том порядке, в котором они объявлены в классе.</P> <P>Позаботившись о явной инициализации объектов встроенных типов, которые не являются членами классов, и обеспечив правильную инициализацию базовых классов и их данных-членов посредством списков инициализации, у вас останется только одна вещь, о чем нужно будет подумать. Речь идет о порядке инициализации нелокальных статических объектов, объявленных в разных единицах трансляции.</P> <P>Отнесемся к этой фразе со всем вниманием.</P> <P><EM>Статический объект</EM> существует от момента, когда был сконструирован, и до конца работы программы. Объекты, размещенные в стеке и в «куче», к статическим не относятся. Статическими являются глобальные объекты, объекты, объявленные в области действия пространства имен, объекты, объявленные с ключевым словом static внутри классов и функций, а также в области действия отдельного файла с исходным текстом. Статические объекты, объявленные внутри функций, известны как <EM>локальные статические объекты</EM> (поскольку они локальны по отношению к функции), а все прочие называют <EM>нелокальными статическими объектами.</EM> Статические объекты автоматически уничтожаются при завершении программы, то есть при выходе из функции main() автоматически вызываются их деструкторы.</P> <P><EM>Единица трансляции (translation unit)</EM> – это исходный код, который порождает отдельный объектный файл. Обычно это один исходный файл плюс все файлы, включенные в него директивой #include.</P> <P>Проблема возникает, когда есть, по крайней мере, два отдельно компилируемых исходных файла, каждый из которых содержит, по крайней мере, один нелокальный статический объект (то есть глобальный объект либо объявленный в области действия пространства имен, класса или файла). Суть ее в том, что если инициализация нелокального статического объекта происходит в одной единице трансляции, а используется он в другой, то такой объект может оказаться неинициализированным в момент использования, поскольку <EM>относительный порядок инициализации нестатических локальных объектов, определенных в разных единицах трансляции, не определен.</EM></P> <P>Рассмотрим пример. Предположим, у вас есть класс FileSystem, который делает файлы из Internet неотличимыми от локальных. Поскольку ваш класс представляет мир как единую файловую систему, вы могли бы создать в глобальной области действия или в пространстве имен соответствующий ей специальный объект:</P> <source lang="cpp"> class FileSystem { // из вашей библиотеки public: ... std::size_t numDisks() const; // одна из многих функций-членов ... }; extern FileSystem tfs; // объект для использования клиентами // “tfs” = “the file system” </source> <P>Класс FileSystem определенно не тривиален, поэтому использование объекта theFileSystem до того, как он будет сконструирован, приведет к катастрофическим последствиям.</P> <P>Теперь предположим, что некий пользователь создает класс, описывающий каталоги файловой системы. Естественно, его класс будет использовать объект theFileSystem:</P> <source lang="cpp"> class Directory { // создан пользователем public: Directory( params ); ... }; Directory::Directory( params ) { ... std::size_t disks = tfs.numDisks(); // использование объекта tfs ... } </source> <P>Далее предположим, что пользователь решает создать отдельный глобальный объект класса Directory, представляющий каталог для временных файлов:</P> <source lang="cpp"> Directory tempDir( params ); // каталог для временных файлов </source> <P>Теперь проблема порядка инициализации становится очевидной: если объект tfs не инициализирован раньше, чем tempDir, то конструктор tempDir попытается использовать tfs до его инициализации. Но tfs и tempDir были созданы разными людьми в разное время и находятся в разных исходных файлах – это нелокальные статические объекты, определенные в разных единицах трансляции. Как вы можете быть уверены, что tfs будет инициализирован раньше, чем tempDir?</P> <P>Да никак! Еще раз повторю: <EM>относительный порядок инициализации нестатических локальных объектов, определенных в разных единицах трансляции, не определен.</EM> На то есть своя причина. Определить «правильный» порядок инициализации нелокальных статических объектов трудно. Очень трудно. Неразрешимо трудно. В наиболее общем случае – при наличии многих единиц трансляции и нелокальных статических объектов, сгенерированных путем неявной конкретизации шаблонов (которые и сами могут быть результатом неявной конкретизации других шаблонов) – не только невозможно определить правильный порядок инициализации, но обычно даже не стоит искать частные случаи, когда этот порядок в принципе определить можно.</P> <P>К счастью, небольшое изменение в проекте программы позволяет полностью устранить эту проблему. Нужно лишь переместить каждый нелокальный статический объект в отдельную функцию, в которой он будет объявлен статическим. Эти функции возвращают ссылки на объекты, которые в них содержатся. Клиенты затем вызывают функции вместо непосредственного обращения к объектам. Другими словами, нелокальные статические объекты заменяются <EM>локальными</EM> статическими объектами (знакомые с паттернами проектирования легко узнают в этом описании типичную реализацию паттерна Singleton).</P> <P>Этот подход основан на том, что C++ гарантирует: локальные статические объекты инициализируются в первый раз, когда определение объекта встречается при вызове этой функции. Поэтому если вы замените прямой доступ к нелокальным статическим объектам вызовом функций, возвращающих ссылки на расположенные внутри них локальные статические объекты, то можете быть уверены, что ссылки, возвращаемые из функций, будут ссылаться на инициализированные объекты. Дополнительное преимущество заключается в том, что если вы никогда не вызываете функцию, эмулирующую нелокальный статический объект, то и не придется платить за создание и уничтожение объекта, чего не скажешь о реальных нелокальных статических объектах.</P> <P>Вот как этот прием применяется к объектам tfs и tempDir:</P> <source lang="cpp"> class FileSystem {...}; // как раньше FileSystem& tfs() // эта функция заменяет объект tfs, она может { // быть статической в классе FileSystem static FileSystem fs; // определение и инициализация локального // статического объекта return fs; // возврат ссылки на него } class Directory {...}; // как раньше Directory::Directory( params ) // как раньше, но вместо ссылки на tfs { // вызов tfs() ... std::size_t disks = tfs().numDisks(); ... } Directory& tempDir() // эта функция заменяет объект tempDir, { // может быть статической в классе Directory static Directory td; // определение/инициализация локального // статического объекта return td; // возврат ссылки на него } </source> <P>Клиенты работают с этой модифицированной программой так же, как раньше, за исключением того, что вместо tfs и tempDir они теперь обращаются к tfs() и tempDir(). Иными словами, используют ссылки на объекты, возвращенные функциями, вместо использования самих объектов.</P> <P>Функции, которые в соответствии с данной схемой возвращают ссылки, всегда просты: определить и инициализировать локальный статический объект в строке 1 и вернуть его в строке 2. В связи с этим у вас может возникнуть искушение объявить их встроенными, особенно, если они часто вызываются ([[Правило 30: Тщательно обдумывайте использование встроенных функций | см. правило 30]]). С другой стороны, тот факт, что эти функции содержат в себе статические объекты, усложняет их применение в многопоточных системах. Но тут никуда не деться: неконстантные статические объекты любого рода – локальные или нелокальные – представляют проблему в случае наличия в программе нескольких потоков. Решить ее можно, например, вызвав самостоятельно все функции, возвращающие ссылки, на этапе запуска программы, когда еще работает только один поток. Это исключит неопределенность в ходе инициализации.</P> <P>Конечно, применимость идеи функций, возвращающих ссылки, для предотвращения проблем, связанных с порядком инициализации, зависит от того, существует ли в принципе разумный порядок инициализации ваших объектов. Если вы напишете код, в котором объект A должен быть инициализирован прежде, чем объект B, и одновременно сделаете инициализацию A зависимой от инициализации B, то вас ждут проблемы – и поделом! Если, однако, вы будете избегать таких патологических ситуаций, то описанная схема сослужит вам добрую службу, по крайней мере, в однопоточных приложениях.</P> <P>Таким образом, чтобы избежать использования объектов до их инициализации, вам следует сделать три вещи. Первое: вручную инициализировать не являющиеся членами объекты встроенных типов. Второе: использовать списки инициализации членов для всех частей объекта. И наконец, третье: обойти за счет правильного проектирования проблему негарантированного порядка инициализации нелокальных статических объектов, определенных в разных единицах трансляции.</P> == Что следует помнить == *Всегда вручную инициализировать объекты встроенных типов, поскольку C++ делает это, только не всегда. *В конструкторе отдавать предпочтение применению списков инициализации членов перед прямым присваиванием значений в теле конструктора. Перечисляйте данные-члены в списке инициализации в том же порядке, в каком они объявлены в классе. *Избегайте проблем с порядком инициализации в разных единицах трансляции, заменяя нелокальные статические объекты локальными статическими объектами. 641074b4cd68f166102d747e85a056fe6f59cfc8 Правило 5: Какие функции C++ создает и вызывает молча 0 8 22 2013-06-07T13:46:30Z 80.76.185.8 0 Новая страница: «<P>Когда пустой класс перестает быть пустым? Когда за него берется C++. Если вы не объявите …» wikitext text/x-wiki <P>Когда пустой класс перестает быть пустым? Когда за него берется C++. Если вы не объявите конструктор копирования, оператор присваивания или деструктор самостоятельно, то компилятор сделает это за вас. Более того, если вы не объявите вообще никакого конструктора, то компилятор автоматически создаст конструктор по умолчанию. Все эти функции будут открытыми и встроенными ([[Правило 30: Тщательно обдумывайте использование встроенных функций | см. правило 30]]). Например, такое объявление:</P> <source lang="cpp"> class Empty {}; </source> <P>эквиваленто следующему:</P> <source lang="cpp"> class Empty { public: Empty() {...} // конструктор по умолчанию Empty(const Empty& rhs) {...} // конструктор копирования ~Empty() {...} // деструктор – см. ниже // о виртуальных деструкторах Empty& operator=(const Empty& rhs) {...} // оператор присваивания }; </source> <P>Эти функции генерируются, только если они нужны, но мало найдется случаев, когда без них можно обойтись. Так, следующий код приведет к их автоматической генерации компилятором:</P> <source lang="cpp"> Empty e1; // конструктор по умолчанию; // деструктор Empty e2(e1); // конструктор копирования e2 = e1; // оператор присваивания </source> <P>Итак, компилятор пишет эти функции для вас, но что они делают? Конструктор по умолчанию и деструктор – это места, в которые компилятор помещает служебный код, например вызов конструкторов и деструкторов базовых классов и нестатических данных-членов. Отметим, что сгенерированный деструктор не является виртуальным ([[Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе | см. правило 7]]), если только речь не идет о классе, наследующем классу, у которого есть виртуальный деструктор (в этом случае виртуальность наследуется от базового класса).</P> <P>Что касается конструктора копирования и оператора присваивания, то сгенерированные компилятором версии просто копируют каждый нестатический член данных исходного объекта в целевой. Например, рассмотрим шаблон NamedObject, который позволяет ассоциировать имена с объектами типа T:</P> <source lang="cpp"> Template<typename T> class NamedObject { public: NamedObject(const char *name, const T& value); NamedObject(const std::string& name, const T& value); ... private: std:string nameValue; T objectValue; }; </source> <P>Поскольку в классе NamedObject объявлен конструктор, компилятор не станет генерировать конструктор по умолчанию. Это важно. Значит, если вы спроектировали класс так, что его конструктору обязательно должны быть переданы какие-то аргументы, то вам не нужно беспокоиться, что компилятор проигнорирует ваше решение и по собственной инициативе добавит еще и конструктор без аргументов.</P> <P>В классе NamedObject нет ни конструктора копирования, ни оператора присваивания, поэтому компилятор сгенерирует их (при необходимости). Посмотрите на следующее употребление конструктора копирования:</P> <source lang="cpp"> NamedObject<int>no1(“Smallest Prime Number”, 2); NamedObject<int>no2(no1); // вызывается конструктор копирования </source> <P>Конструктор копирования, сгенерированный компилятором, должен инициализировать no2.nameValue и no2.objectValue, используя nol.nameValue и nol.objectValue соответственно. Член nameValue имеет тип string, а в стандартном классе string объявлен конструктор копирования, поэтому no2. nameValue будет инициализирован вызовом конструктора копирования string с аргументов nol.nameValue. С другой стороны, член NameObject&lt;int&gt;::objectValue имеет тип int (поскольку T есть int в данной конкретизации шаблона), а int – встроенный тип, поэтому no2.objectValue будет инициализирован побитовым копированием nol.objectValue.</P> <P>Сгенерированный компилятором оператор присваивания для класса Named-Object&lt;int&gt; будет вести себя аналогичным образом, но, вообще говоря, сгенерированная компилятором версия оператора присваивания ведет себя так, как я описал, только в том случае, когда в результате получается корректный и осмысленный код. В противном случае компилятор не сгенерирует operator=.</P> <P>Например, предположим, что класс NamedObject определен, как показано ниже. Обратите внимание, что nameValue – ссылка на string, а objectValue имеет тип const T:</P> <source lang="cpp"> template<class T> class NamedObject { public: // этот конструктор более не принимает const name, поскольку nameValue – // теперь ссылка на неконстантную строку. Конструктор с аргументом типа // char* исключен, поскольку нам нужна строка, на которую можно сослаться NamedObject(std::string& name, const T& value); ... // как и ранее, предполагаем, // что operator= не объявлен private: std::string& nameValue; // теперь это ссылка const T objectValue; // теперь const }; </source> <P>Посмотрим, что произойдет в приведенном ниже коде:</P> <source lang="cpp"> std::string newDog(“Persephone”); std::string oldDog(“Satch”); NamedObject<int> p(newDog, 2); // Когда я впервые написал это, // наша собака Персефона собиралась // встретить свой второй день рождения NamedObject<int> s(oldDog, 36); // Семейному псу Сатчу (из моего // детства) было бы теперь 36 лет p = s; // Что должно произойти // с данными-членами p? </source> <P>Перед присваиванием и p.nameValue, и s.nameValue ссылались на объекты string, хотя и на разные. Что должно произойти с членом p.nameValue в результате присваивания? Должен ли он ссылаться на ту же строку, что и s.nameValue, то есть должна ли модифицироваться ссылка? Если да, это подрывает основы, потому что C++ не позволяет изменить объект, на который указывает ссылка. Но, быть может, должна модифицироваться строка, на которую ссылается член p.nameValue, и тогда будут затронуты другие объекты, содержащие указатели или ссылки на эту строку, хотя они и не участвовали непосредственно в присваивании? Это ли должен делать сгенерированный компилятором оператор присваивания?</P> <P>Сталкиваясь с подобной головоломкой, C++ просто отказывается компилировать этот код. Если вы хотите поддерживать присваивание в классе, включающем в себя член-ссылку, то должны определить оператор присваивания самостоятельно. Аналогичным образом компилятор ведет себя с классами, содержащими константные члены (такие как objectValue во втором варианте класса NamedObject выше). Модифицировать константные члены запрещено, поэтому компилятор не знает, как поступать при неявной генерации оператора присваивания. Кроме того, компилятор не станет неявно генерировать оператор присваивания в производном классе, если в его базовом объявлен закрытый оператор присваивания. И наконец, предполагается, что сгенерированные компилятором операторы присваивания для производных классов должны обрабатывать части базовых классов ([[Правило 12: Копируйте все части объекта | см. правило 12]]), но при этом они конечно же не могут вызывать функции-члены, доступ к которым для них запрещен.</P> == Что следует помнить == *Компилятор может неявно генерировать для класса конструктор по умолчанию, конструктор копирования, оператор присваивания и деструктор. 8868e5e81fd0f0634197c2b0dc1331a0464ac16e Правило 6: Явно запрещайте компилятору генерировать функции, которые вам не нужны 0 9 25 2013-06-10T06:48:30Z Lerom 3360334 Новая страница: «<P>Агенты по продаже недвижимости и программные системы, обслуживающие их деятельность, …» wikitext text/x-wiki <P>Агенты по продаже недвижимости и программные системы, обслуживающие их деятельность, могут нуждаться в классе, представляющем дома, выставленные на продажу:</P> <source lang="cpp"> class HomeForSale {...}; </source> <BR><P>Любой агент по продаже недвижимости скажет вам, что каждый объект уникален – не бывает двух, в точности одинаковых. Вот почему идея создания копии объекта HomeForSale бессмысленна. Как можно скопировать нечто, по определению, уникальное? Поэтому хотелось бы, чтобы попытки скопировать объекты HomeForSale не компилировались:</P> <source lang="cpp"> HomeForSale h1; HomeForSale h2; HomeForSale h3(h1); // попытка скопировать h1 – // не должно компилироваться! h1 = h2; // попытка скопировать h2 – // не должно компилироваться! </source> <P>Увы, предотвратить такую компиляцию не так-то просто. Обычно, если вы не хотите, чтобы класс поддерживал определенного рода функциональность, вы просто не объявляете функций, которые ее реализуют. Но с конструктором копирования и оператором присваивания эта стратегия не работает, поскольку, как следует из [[Правило 5: Какие функции C++ создает и вызывает молча | правила 5]], если вы их не объявляете, а где-то в программе производится попытка их вызвать, то компилятор сгенерирует их автоматически.</P> <P>Похоже на безвыходное положение. Если вы сами не объявите конструктор копирования или оператор присваивания, то их сгенерирует компилятор. И ваш класс будет поддерживать копирование. Но то же самое произойдет, если вы объявите эти функции самостоятельно. Однако наша цель – <EM>предотвратить</EM> копирование!</P> <P>Ключ к решению в том, что все сгенерированные компилятором функции являются открытыми. Чтобы предотвратить автоматическое генерирование, вы должны объявить их самостоятельно, но никто не требует, чтобы они были открытыми. Ну так и объявите конструктор копирования и оператор присваивания <EM>закрытыми.</EM> Объявляя явно функцию-член, вы предотвращаете генерирование ее компилятором, а сделав ее закрытой, не позволяете кому-либо вызывать ее.</P> <P>Схема не идеальна, потому что другие члены класса и функции-друзья по-прежнему могут вызывать закрытые функции. <EM>Если только</EM> вы не включите лишь объявление, опустив определение. Тогда если кто-то случайно вызовет такую функцию, то получит сообщение об ошибке на этапе компоновки. Этот трюк – объявление функций-членов закрытыми и сознательный отказ от их реализации – как раз и используется для предотвращения копирования в некоторых классах библиотеки iostreams. Взгляните, например, на объявления классов ios_base, basic_ios и sentry в вашей реализации стандартной библиотеки. Вы обнаружите, что в каждом случае как конструктор копирования, так и оператор присваивания объявлены закрытыми и нигде не определены.</P> <P>Применить эту уловку в классе HomeForSale несложно:</P> <source lang="cpp"> class HomeForSale { public: ... private: HomeForSale(const HomeForSale&); // только объявления HomeForSale& oparetor=( const HomeForSale&); }; </source> <P>Заметьте, что я не указал имена параметров функций. Это необязательно, просто таково общее соглашение. Ведь раз эти функции никогда не будут реализовываться и использоваться, то какой смысл задавать имена их параметров?</P> <P>При таком определении компилятор будет блокировать любые попытки клиентов копировать объекты HomeForSale, а если вы случайно попытаетесь сделать это в функции-члене или функции-друге класса, то об ошибке сообщит компоновщик.</P> <P>Существует возможность переместить ошибку с этапа компоновки на этап компиляции (это всегда полезно – лучше обнаружить ошибку как можно раньше), если объявить конструктор копирования и оператор присваивания закрытыми не в самом классе HomeForSale, а в его базовом классе, специально созданном для предотвращения копирования. Такой базовый класс очень прост:</P> <source lang="cpp"> class Uncopyable { protected: Uncopyable() {} // разрешить конструирование ~Uncopyable() {} // и уничтожение // объектов производных классов private: Uncopyable(const Uncopyable&); // но предотвратить копирование Uncopyable& operator=(const Uncopyable&); }; </source> <P>Чтобы предотвратить копирование объектов HomeForSale, нужно лишь унаследовать его от Uncopyable:</P> <source lang="cpp"> class HomeForSale : private Uncopyable { // в этом класс больше нет ни ... // конструктора копирования, ни } // оператора присваивания </source> <P>Такое решение работает, потому что компилятор пытается генерировать конструктор копирования и оператор присваивания, если где-то – пусть даже в функции-члене или дружественной функции – производится попытка скопировать объект HomeForSale. Как объясняется в [[Правило 12: Копируйте все части объекта | правиле 12]], сгенерированные компилятором версии будут вызывать соответствующие функции из базового класса. Но это не получится, так как в базовом классе они объявлены закрытыми.</P> <P>Реализация и использование класса Uncopyable сопряжена с некоторыми тонкостями. Например, наследование от Uncopyable не должно быть открытым (см. [[Правило 32: Используйте открытое наследование для моделирования отношения «является» | правила 32]] и [[Правило 39: Продумывайте подход к использованию закрытого наследования | 39]]), а деструктор Uncopyable не должен быть виртуальным ([[Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе | см. правило 7]]). Поскольку Uncopyable не имеет данных-членов, то компилятор может прибегнуть к оптимизации пустых базовых классов, описанной в [[Правило 39: Продумывайте подход к использованию закрытого наследования | правиле 39]], но коль скоро этот класс базовый, то возможно возникновение множественного наследования ([[Правило 40: Продумывайте подход к использованию множественного наследования | см. правило 40]]). А множественное наследование в некоторых случаях не дает возможности провести оптимизацию пустых базовых классов ([[Правило 39: Продумывайте подход к использованию закрытого наследования | см. правило 39]]). Вообще говоря, вы можете игнорировать эти тонкости и просто использовать Uncopyable, как показано выше. Можете также воспользоваться версией из билиотеки Boost ([[Правило 55: Познакомьтесь с Boost | см. правило 55]]). В ней этот класс называется noncopyable. Это хороший класс, но мне просто показалось, что его название немного, скажем так, неестественное.</P> == Что следует помнить == *Чтобы отключить функциональность, автоматически предоставляемую компилятором, объявите соответствующую функцию-член закрытой и не включайте ее реализацию. Наследование базовому классу типа Uncopyable – один из способов сделать это. 93a34c02baa21e051879a23d28928e4949ce0c8d Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе 0 10 26 2013-06-10T07:10:09Z Lerom 3360334 Новая страница: «Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе <P>Существуе…» wikitext text/x-wiki Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе <P>Существует много способов отслеживать время, поэтому имеет смысл создать базовый класс TimeKeeper и производные от него классы, которые реализуют разные подходы к хронометражу:</P> <source lang="cpp"> class TimeKeeper { public: TimeKeeper(); ~TimeKeeper(); ... }; class AtomicClock: public TimeKeeper {…}; class WaterClock: public TimeKeeper {….}; class WristWatch: public TimeKeeper {…}; </source> <P>Многие клиенты захотят иметь доступ к данным о времени, не заботясь о деталях того, как они получаются, поэтому мы можем воспользоваться <EM>фабричной функцией (factory function),</EM> которая возвращает указатель на базовый класс созданного ей объекта производного класса:</P> <source lang="cpp"> TimeKeeper *getTimeKeeper(); // возвращает указатель на динамически // выделенный объект класса, // производного от TimeKeeper </source> <P>В соответствии с соглашением о фабричных функциях объекты, возвращаемые getTimeKeeper, выделяются из кучи, поэтому для того, чтобы избежать утечек памяти и других ресурсов, важно, чтобы каждый полученный объект был рано или поздно уничтожен:</P> <source lang="cpp"> TomeKeeper *ptk = getTimeKeeper(); // получить динамически выделенный // объект из иерархии TimeKeeper ... // использовать его delete ptk; // уничтожить, чтобы избежать утечки // ресурсов </source> <P>Как объясняется в [[Правило 13: Используйте объекты для управления ресурсами | правиле 13]], полагаться на то, что объект уничтожит клиент, чревато ошибками, а в [[Правило 18: Проектируйте интерфейсы так, что их легко было использовать правильно и трудно – неправильно | правиле 18]] говорится, как можно модифицировать фабричную функцию для предотвращения наиболее частых ошибок в клиентской программе. Здесь же мы обсудим более серьезный недостаток приведенного выше кода: даже если клиент все делает правильно, мы не можем узнать, как будет вести себя программа.</P> <P>Проблема в том, что getTimeKeeper возвращает указатель на объект производного класса (например, AtomicClock), а удалять этот объект нужно через указатель на базовый класс (то есть на TimeKeeper), при этом в базовом классе (TimeKeeper) объявлен невиртуальный деструктор. Это прямой путь к неприятностям, потому что в спецификации C++ постулируется, что когда объект производного класса уничтожается через указатель на базовый класс с невиртуальным деструктором, то результат не определен. Во время исполнения это обычно приводит к тому, что часть объекта, принадлежащая производному классу, никогда не будет уничтожена. Если getTimeKeeper() возвращает указатель на объект класс AtomicClock, то часть объекта, принадлежащая AtomicClock (то есть данные-члены, объявленные в этом классе), вероятно, не будут уничтожены, так как не будет вызван деструктор AtomicClock. Те же члены, что относятся к базовому классу (то есть TimeKeeper), будут уничтожены, что приведет к появлению так называемых «частично разрушенных» объектов. Это верный путь к утечке ресурсов, повреждению структур данных и проведению изрядного времени в обществе отладчика.</P> <P>Решить эту проблему легко: нужно объявить в базовом классе виртуальный деструктор. Тогда при удалении объектов производных классов будет происходить именно то, что нужно. Объект будет разрушен целиком, включая все его части:</P> <source lang="cpp"> class TimeKeeper { public: TimeKeeper(); virtual ~TimeKeeper(); ... }; TimeKeeper *ptk = get TimeKeeper(); ... delete ptk; // теперь работает правильно </source> <P>Обычно базовые классы вроде TimeKeeper содержат и другие виртуальные функции, кроме деструктора, поскольку назначение виртуальных функций – обеспечить возможность настройки производных классов ([[Правило 34: Различайте наследование интерфейса и наследование реализации | см. правило 34]]). Например, в классе TimeKeeper может быть определена виртуальная функция getCurrentTime, реализованная по-разному в разных производных классах. Любой класс с виртуальными функциями почти наверняка должен иметь виртуальный деструктор.</P> <P>Если же класс не имеет виртуальных функций, это часто означает, что он не предназначен быть базовым. А в таком классе определять виртуальный деструктор не стоит. Рассмотрим класс, представляющий точку на плоскости:</P> <source lang="cpp"> class Point { // точка на плоскости public: Point(int xCoord, int yCoord); ~Point(); private: int x,y; }; </source> <P>Если int занимает 32 бита, то объект Point обычно может поместиться в 64-битовый регистр. Более того, такой объект Point может быть передан как 64-битовое число функциям, написанным на других языках (таких как C или FORTRAN). Если же деструктор Point сделать виртуальным, то ситуация изменится.</P> <P>Для реализации виртуальных функций необходимо, чтобы в объекте хранилась информация, которая во время исполнения позволяет определить, какая виртуальная функция должна быть вызвана. Эта информация обычно представлена указателем на таблицу виртуальных функций vptr (virtual table pointer). Сама таблица – это массив указателей на функции, называемый vtbl (virtual table). С каждым классом, в котором определены виртуальные функции, ассоциирована таблица vtbl. Когда для некоторого объекта вызывается виртуальная функция, то с помощью указателя vptr в таблице vtbl ищется та реальная функция, которую нужно вызвать.</P> <P>Детали реализации виртуальных функций не важны. Важно то, что если класс Point содержит виртуальную функцию, то объект этого типа увеличивается в размере. В 32-битовой архитектуре его размер возрастает с 64 бит (два целых int) до 96 бит (два int плюс vptr); в 64-битовой архитектуре он может вырасти с 64 до 128 бит, потому что указатели в этой архитектуре имеют размер 64 бита. Таким образом, добавление vptr к объекту Point увеличивает его размер на величину от 50 до 100 %! После этого объект Point уже не может поместиться в 64-битный регистр. Более того, объекты этого типа в C++ перестают выглядеть так, как аналогичные структуры, объявленные на других языках, например на C, потому что в других языках нет понятия vptr. В результате становится невозможно передавать объекты типа Point написанным на других языках программам, если только вы не учтете наличия vptr. А это уже деталь реализации, и, следовательно, такой код не будет переносимым.</P> <P>Практический вывод из всего вышесказанного состоит в том, что необоснованно объявлять все деструкторы виртуальными так же неверно, как не объявлять их виртуальными никогда. Можно высказать этот совет и в таком виде: деструкторы следует объявлять виртуальными тогда, когда в классе есть хотя бы одна виртуальная функция.</P> <P>Однако невиртуальные деструкторы могут стать причиной неприятностей даже при полном отсутствии в классе виртуальных функций. Например, в стандартном классе string нет виртуальных функций, но программисты временами все же используют его как базовый класс:</P> <source lang="cpp"> class SpecialString: public std::string { // плохо! std::string содержит ... // невиртуальный деструктор }; </source> <P>На первый взгляд такой код может показаться безвредным, но если где-то в приложении вы преобразуете указатель на SpecialString в указатель на string, а затем выполните для этого указателя delete, то немедленно попадете в область неопределенного поведения:</P> <source lang="cpp"> SpecialString *pss = new SpecialString(“Надвигающаяся опасность”); std::string *ps; ... ps = pss; // SpecialString*=>std::string* ... delete ps; // неопределенность! На практике ресурсы, выделенные // объекту SpecialString, не будут освобождены, потому // что деструктор SpecialString не вызывается </source> <P>То же относится к любому классу, в котором нет виртуального деструктора, в частности ко всем типам STL-контейнеров (например, vector, list, set, tr1::unordered_map [[[Правило 54: Ознакомьтесь со стандартной библиотекой, включая TR1 | см. правило 54]]] и т. д.). Если у вас когда-нибудь возникнет соблазн унаследовать стандартному контейнеру или любому другому классу с невиртуальным деструктором, воздержитесь! (К сожалению, в C++ не предусмотрено никакого механизма предотвращения наследования, как, скажем, final в языке Java, или sealed в C#).</P> <P>Иногда может быть удобно добавить в класс чисто виртуальный деструктор. Вспомним, что чисто виртуальные функции порождают <EM>абстрактные</EM> классы, то есть классы, экземпляры которых создать нельзя. Иногда, однако, у вас есть класс, который вы хотели бы сделать абстрактным, но в нем нет ни одной пустой виртуальной функции. Что делать? Поскольку абстрактный класс предназначен для использования в качестве базового и поскольку базовый класс должен иметь виртуальный деструктор, а чисто виртуальная функция порождает абстрактный класс, то решение очевидно: объявить чисто виртуальный деструктор в классе, который вы хотите сделать абстрактным. Вот пример:</P> <source lang="cpp"> class AWOV { // AWOV = “Abstract w/o Virtuals” public: virtual ~AWOV() = 0; // объявление чисто виртуального }; // деструктора </source> <P>Этот класс включает в себя чисто виртуальную функцию, поэтому он абстрактный. А раз в нем объявлен виртуальный деструктор, то можно не беспокоиться о том, что деструкторы базовых классов не будут вызваны. Однако есть одна тонкость: вы должны предоставить <EM>определение</EM> чисто виртуального деструктора:</P> <source lang="cpp"> AWOV::~AWOV(){}; // определение чисто виртуального деструктора </source> <P>Дело в том, что сначала всегда вызывается деструктор «самого производного» класса (то есть находящегося на нижней ступени иерархии наследования), а затем деструкторы каждого базового класса. Компилятор сгенерирует вызов ~AWOV из деструкторов производных от него классов, а значит, вы должны позаботиться о его реализации. Если этого не сделать, компоновщик будет недоволен.</P> <P>Правило включения в базовые классы виртуальных деструкторов касается только <EM>полиморфных</EM> базовых классов, то есть таких, которые позволяют манипулировать объектами производных классов с помощью указателя на базовый. TimeKeeper – полиморфный базовый класс, мы ожидаем, что при наличии указателя на объект TimeKeeper сможем манипулировать объектами AtomicClock и WaterClock.</P> <P>Не все базовые классы разрабатываются с учетом полиморфизма. Например, и стандартный тип string, и типы STL-контейнеров спроектированы так, что не допускают возможности использования в качестве базовых, так как не являются полиморфными. Некоторые классы предназначены служить в качестве базовых, но полиморфно использоваться не могут; примером могут служить класс Uncopyable из правила 6 и класс input_iterator_tag из стандартной библиотеки (см. правило 47). Таким классам не нужны виртуальные деструкторы.</P> <H2><STRONG><EM><a name=label28 style="border:none;"></a>Что следует помнить</EM></STRONG></H2><P>• Полиморфные базовые классы должны объявлять виртуальные деструкторы. Если класс имеет хотя бы одну виртуальную функцию, он должен иметь виртуальный деструктор.</P> <P>• В классах, не предназначенных для использования в качестве базовых или для полиморфного применения, не следует объявлять виртуальные деструкторы.</P> 064c4168831869de32f686cfb73af50517a1a476 27 26 2013-06-10T07:31:57Z Lerom 3360334 wikitext text/x-wiki Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе <P>Существует много способов отслеживать время, поэтому имеет смысл создать базовый класс TimeKeeper и производные от него классы, которые реализуют разные подходы к хронометражу:</P> <source lang="cpp"> class TimeKeeper { public: TimeKeeper(); ~TimeKeeper(); ... }; class AtomicClock: public TimeKeeper {…}; class WaterClock: public TimeKeeper {….}; class WristWatch: public TimeKeeper {…}; </source> <P>Многие клиенты захотят иметь доступ к данным о времени, не заботясь о деталях того, как они получаются, поэтому мы можем воспользоваться <EM>фабричной функцией (factory function),</EM> которая возвращает указатель на базовый класс созданного ей объекта производного класса:</P> <source lang="cpp"> TimeKeeper *getTimeKeeper(); // возвращает указатель на динамически // выделенный объект класса, // производного от TimeKeeper </source> <P>В соответствии с соглашением о фабричных функциях объекты, возвращаемые getTimeKeeper, выделяются из кучи, поэтому для того, чтобы избежать утечек памяти и других ресурсов, важно, чтобы каждый полученный объект был рано или поздно уничтожен:</P> <source lang="cpp"> TomeKeeper *ptk = getTimeKeeper(); // получить динамически выделенный // объект из иерархии TimeKeeper ... // использовать его delete ptk; // уничтожить, чтобы избежать утечки // ресурсов </source> <P>Как объясняется в [[Правило 13: Используйте объекты для управления ресурсами | правиле 13]], полагаться на то, что объект уничтожит клиент, чревато ошибками, а в [[Правило 18: Проектируйте интерфейсы так, что их легко было использовать правильно и трудно – неправильно | правиле 18]] говорится, как можно модифицировать фабричную функцию для предотвращения наиболее частых ошибок в клиентской программе. Здесь же мы обсудим более серьезный недостаток приведенного выше кода: даже если клиент все делает правильно, мы не можем узнать, как будет вести себя программа.</P> <P>Проблема в том, что getTimeKeeper возвращает указатель на объект производного класса (например, AtomicClock), а удалять этот объект нужно через указатель на базовый класс (то есть на TimeKeeper), при этом в базовом классе (TimeKeeper) объявлен невиртуальный деструктор. Это прямой путь к неприятностям, потому что в спецификации C++ постулируется, что когда объект производного класса уничтожается через указатель на базовый класс с невиртуальным деструктором, то результат не определен. Во время исполнения это обычно приводит к тому, что часть объекта, принадлежащая производному классу, никогда не будет уничтожена. Если getTimeKeeper() возвращает указатель на объект класс AtomicClock, то часть объекта, принадлежащая AtomicClock (то есть данные-члены, объявленные в этом классе), вероятно, не будут уничтожены, так как не будет вызван деструктор AtomicClock. Те же члены, что относятся к базовому классу (то есть TimeKeeper), будут уничтожены, что приведет к появлению так называемых «частично разрушенных» объектов. Это верный путь к утечке ресурсов, повреждению структур данных и проведению изрядного времени в обществе отладчика.</P> <P>Решить эту проблему легко: нужно объявить в базовом классе виртуальный деструктор. Тогда при удалении объектов производных классов будет происходить именно то, что нужно. Объект будет разрушен целиком, включая все его части:</P> <source lang="cpp"> class TimeKeeper { public: TimeKeeper(); virtual ~TimeKeeper(); ... }; TimeKeeper *ptk = get TimeKeeper(); ... delete ptk; // теперь работает правильно </source> <P>Обычно базовые классы вроде TimeKeeper содержат и другие виртуальные функции, кроме деструктора, поскольку назначение виртуальных функций – обеспечить возможность настройки производных классов ([[Правило 34: Различайте наследование интерфейса и наследование реализации | см. правило 34]]). Например, в классе TimeKeeper может быть определена виртуальная функция getCurrentTime, реализованная по-разному в разных производных классах. Любой класс с виртуальными функциями почти наверняка должен иметь виртуальный деструктор.</P> <P>Если же класс не имеет виртуальных функций, это часто означает, что он не предназначен быть базовым. А в таком классе определять виртуальный деструктор не стоит. Рассмотрим класс, представляющий точку на плоскости:</P> <source lang="cpp"> class Point { // точка на плоскости public: Point(int xCoord, int yCoord); ~Point(); private: int x,y; }; </source> <P>Если int занимает 32 бита, то объект Point обычно может поместиться в 64-битовый регистр. Более того, такой объект Point может быть передан как 64-битовое число функциям, написанным на других языках (таких как C или FORTRAN). Если же деструктор Point сделать виртуальным, то ситуация изменится.</P> <P>Для реализации виртуальных функций необходимо, чтобы в объекте хранилась информация, которая во время исполнения позволяет определить, какая виртуальная функция должна быть вызвана. Эта информация обычно представлена указателем на таблицу виртуальных функций vptr (virtual table pointer). Сама таблица – это массив указателей на функции, называемый vtbl (virtual table). С каждым классом, в котором определены виртуальные функции, ассоциирована таблица vtbl. Когда для некоторого объекта вызывается виртуальная функция, то с помощью указателя vptr в таблице vtbl ищется та реальная функция, которую нужно вызвать.</P> <P>Детали реализации виртуальных функций не важны. Важно то, что если класс Point содержит виртуальную функцию, то объект этого типа увеличивается в размере. В 32-битовой архитектуре его размер возрастает с 64 бит (два целых int) до 96 бит (два int плюс vptr); в 64-битовой архитектуре он может вырасти с 64 до 128 бит, потому что указатели в этой архитектуре имеют размер 64 бита. Таким образом, добавление vptr к объекту Point увеличивает его размер на величину от 50 до 100 %! После этого объект Point уже не может поместиться в 64-битный регистр. Более того, объекты этого типа в C++ перестают выглядеть так, как аналогичные структуры, объявленные на других языках, например на C, потому что в других языках нет понятия vptr. В результате становится невозможно передавать объекты типа Point написанным на других языках программам, если только вы не учтете наличия vptr. А это уже деталь реализации, и, следовательно, такой код не будет переносимым.</P> <P>Практический вывод из всего вышесказанного состоит в том, что необоснованно объявлять все деструкторы виртуальными так же неверно, как не объявлять их виртуальными никогда. Можно высказать этот совет и в таком виде: деструкторы следует объявлять виртуальными тогда, когда в классе есть хотя бы одна виртуальная функция.</P> <P>Однако невиртуальные деструкторы могут стать причиной неприятностей даже при полном отсутствии в классе виртуальных функций. Например, в стандартном классе string нет виртуальных функций, но программисты временами все же используют его как базовый класс:</P> <source lang="cpp"> class SpecialString: public std::string { // плохо! std::string содержит ... // невиртуальный деструктор }; </source> <P>На первый взгляд такой код может показаться безвредным, но если где-то в приложении вы преобразуете указатель на SpecialString в указатель на string, а затем выполните для этого указателя delete, то немедленно попадете в область неопределенного поведения:</P> <source lang="cpp"> SpecialString *pss = new SpecialString(“Надвигающаяся опасность”); std::string *ps; ... ps = pss; // SpecialString*=>std::string* ... delete ps; // неопределенность! На практике ресурсы, выделенные // объекту SpecialString, не будут освобождены, потому // что деструктор SpecialString не вызывается </source> <P>То же относится к любому классу, в котором нет виртуального деструктора, в частности ко всем типам STL-контейнеров (например, vector, list, set, tr1::unordered_map [[[Правило 54: Ознакомьтесь со стандартной библиотекой, включая TR1 | см. правило 54]]] и т. д.). Если у вас когда-нибудь возникнет соблазн унаследовать стандартному контейнеру или любому другому классу с невиртуальным деструктором, воздержитесь! (К сожалению, в C++ не предусмотрено никакого механизма предотвращения наследования, как, скажем, final в языке Java, или sealed в C#).</P> <P>Иногда может быть удобно добавить в класс чисто виртуальный деструктор. Вспомним, что чисто виртуальные функции порождают <EM>абстрактные</EM> классы, то есть классы, экземпляры которых создать нельзя. Иногда, однако, у вас есть класс, который вы хотели бы сделать абстрактным, но в нем нет ни одной пустой виртуальной функции. Что делать? Поскольку абстрактный класс предназначен для использования в качестве базового и поскольку базовый класс должен иметь виртуальный деструктор, а чисто виртуальная функция порождает абстрактный класс, то решение очевидно: объявить чисто виртуальный деструктор в классе, который вы хотите сделать абстрактным. Вот пример:</P> <source lang="cpp"> class AWOV { // AWOV = “Abstract w/o Virtuals” public: virtual ~AWOV() = 0; // объявление чисто виртуального }; // деструктора </source> <P>Этот класс включает в себя чисто виртуальную функцию, поэтому он абстрактный. А раз в нем объявлен виртуальный деструктор, то можно не беспокоиться о том, что деструкторы базовых классов не будут вызваны. Однако есть одна тонкость: вы должны предоставить <EM>определение</EM> чисто виртуального деструктора:</P> <source lang="cpp"> AWOV::~AWOV(){}; // определение чисто виртуального деструктора </source> <P>Дело в том, что сначала всегда вызывается деструктор «самого производного» класса (то есть находящегося на нижней ступени иерархии наследования), а затем деструкторы каждого базового класса. Компилятор сгенерирует вызов ~AWOV из деструкторов производных от него классов, а значит, вы должны позаботиться о его реализации. Если этого не сделать, компоновщик будет недоволен.</P> <P>Правило включения в базовые классы виртуальных деструкторов касается только <EM>полиморфных</EM> базовых классов, то есть таких, которые позволяют манипулировать объектами производных классов с помощью указателя на базовый. TimeKeeper – полиморфный базовый класс, мы ожидаем, что при наличии указателя на объект TimeKeeper сможем манипулировать объектами AtomicClock и WaterClock.</P> <P>Не все базовые классы разрабатываются с учетом полиморфизма. Например, и стандартный тип string, и типы STL-контейнеров спроектированы так, что не допускают возможности использования в качестве базовых, так как не являются полиморфными. Некоторые классы предназначены служить в качестве базовых, но полиморфно использоваться не могут; примером могут служить класс Uncopyable из [[Правило 6: Явно запрещайте компилятору генерировать функции, которые вам не нужны | правила 6]] и класс input_iterator_tag из стандартной библиотеки ([[Правило 47: Используйте классы-характеристики для предоставления информации о типах | см. правило 47]]). Таким классам не нужны виртуальные деструкторы.</P> == Что следует помнить == *Полиморфные базовые классы должны объявлять виртуальные деструкторы. Если класс имеет хотя бы одну виртуальную функцию, он должен иметь виртуальный деструктор. *В классах, не предназначенных для использования в качестве базовых или для полиморфного применения, не следует объявлять виртуальные деструкторы. 91209d577c9d38b55df846798cea57c07bea06c2 Правило 8: Не позволяйте исключениям покидать деструкторы 0 11 28 2013-06-10T07:56:48Z Lerom 3360334 Новая страница: «<P>C++ не запрещает использовать исключения в деструкторах, но это, безусловно, очень нежел…» wikitext text/x-wiki <P>C++ не запрещает использовать исключения в деструкторах, но это, безусловно, очень нежелательная практика. На то есть серьезная причина. Рассмотрим пример:</P> <source lang="cpp"> class Widget { public: ... ~Widget() {...} // предположим, здесь есть исключение }; void doSomething() { std::vector<Widget> v; ... } // здесь v автоматически уничтожается </source> <P>Когда вектор v уничтожается, он отвечает за уничтожение всех объектов Widget, которые в нем содержатся. Предположим, что v содержит 10 объектов Widget, и во время уничтожения первого из них возбужается исключение. Остальные девять объектов Widget также должны быть уничтожены (иначе ресурсы, выделенные для них, будут потеряны), поэтому необходимо вызвать и их деструкторы. Но представим, что в это время деструктор второго объекта Widget также возбудит исключение. Тогда возникнет сразу два одновременно активных исключения, а это слишком много для C++. В зависимости от конкретных условий исполнение программы либо будет прервано, либо ее поведение окажется неопределенным. В этом примере как раз имеет место второй случай. И так будет происходить при использовании любого библиотечного контейнера (например, list, set), любого контейнера TR1 ([[Правило 54: Ознакомьтесь со стандартной библиотекой, включая TR1 | см. правило 54]]) и даже массива. И причина этой проблемы не в контейнерах или массивах. Преждевременное завершение программы или неопределенное поведение здесь является результатом того, что деструкторы возбуждают исключения. C++ <EM>не</EM> любит деструкторов, возбуждающих исключения!</P> <P>Это достаточно просто понять. Но что вы должны делать, если в вашем деструкторе необходимо выполнить операцию, которая может породить исключение? Например, предположим, что мы имеем дело с классом, описывающим подключение к базе данных:</P> <source lang="cpp"> class DBConnection { public: ... static DBConnection create(); // функция возвращает объект // DBConnection; параметры для // простоты опущены void close(); // закрыть соединение; при неудаче }; // возбуждает исключение </source> <P>Для гарантии того, что клиент не забудет вызвать close для объектов DBConnection, резонно создать класс для управления ресурсами DBConnection, который вызывает close в своем деструкторе. Классы, управляющие ресурсами, мы подробно рассмотрим в главе 3, а здесь достаточно прикинуть, как должен выглядеть деструктор такого класса:</P> <source lang="cpp"> class DBConn { // Класс для управления объектами public: // DBConnection ... ~DBConn() // обеспечить, чтобы соединения с базой { // данных всегда закрывались db.close(); } private: DBConnecton db; }; </source> <P>Тогда клиент может содержать такой код:</P> <source lang="cpp"> { // блок открывается DBConn dbc(DBConnection::create()); // создать объект DBConnection // и передать его объекту DBConn ... // использовать объект DBConnection // через интерфейс DBConn } // в конце блока объект DBConn // уничтожается, при этом // автоматически вызывается метод close // объекта DBConnection </source> <P>Все это приемлемо до тех пор, пока метод close завершается успешно, но если его вызов возбуждает исключение, то оно покидает пределы деструктора DBConn. Это очень плохо, потому что деструкторы, возбуждающие исключения, могут стать источниками ошибок.</P> <P>Есть два основных способа избежать этой проблемы. Деструктор DBConn может:</P> <P>*<STRONG>Прервать программу,</STRONG> если close возбуждает исключение; обычно для этого вызывается функция abort:</P> <source lang="cpp"> DBConn::~DBConn() { try {db.close();} catch(...) { //записать в протокол, что вызов close завершился неудачно; std::abort(); } } </source> <P>Это резонный выбор, если программа не может продолжать работу после того, как в деструкторе произошла ошибка. Преимущество такого подхода – в предотвращении неопределенного поведения. Вызов abort упредит возникновение неопределенности.</P> <P>*<STRONG>Перехватить исключение,</STRONG> возбужденное вызовом close:</P> <source lang="cpp"> DBConn::~DBConn() { try {db.close();} catch(...) { //записать в протокол, что вызов close завершился неудачно; } } </source> <P>Вообще говоря, такое «проглатывание» исключений – плохая идея, потому что мы теряем важную информацию: <EM>что-то не сработало</EM> ! Но иногда лучше поступить так, чтобы избежать преждевременной остановки программы или неопределенного поведения. Выбирать этот подход следует лишь в случае, когда программа в состоянии надежно продолжать исполнение, даже после того, как ошибка произошла, но была проигнорирована.</P> <P>Ни одно из этих решений не является идеальным. Проблема в том, что в обоих случаях программа не имеет возможности отреагировать на ситуацию, которая привела к возбуждению исключения внутри close.</P> <P>Более разумная стратегия – спроектировать интерфейс DBConn так, чтобы его клиенты сами имели возможность реагировать на возникающие ошибки. Например, класс DBConn может предоставить собственную функцию close и таким образом дать клиентам шанс обработать исключение, возникшее в процессе операции. Объект этого класса мог бы отслеживать, было ли соединение DBConnection уже закрыто функцией close, и, если это не так, закрывать его в деструкторе. Тем самым предотвращается утечка соединений. Но если close все-таки будет вызвана из деструктора и возбудит исключение, то мы опять возвращаемся к описанным выше вариантам: прервать программу или «проглотить» исключение:</P> <source lang="cpp"> class DBConn { public: ... void close() // новая функция для использования клиентом { db.close() closed = true; } ~DBConn() { if(!closed) try { db.close(); // закрыть соединение, если этого не сделал } // клиент catch(...) { // если возникнет исключение, запротоколировать записать в протокол, // и прервать программу или «проглотить» его что вызов close завершился неудачно; } } private: DBConnecton db; bool closed; }; </source> <P>Перемещение вызова close из деструктора DBConn в код клиента (и оставлением в деструкторе DBConn «страховочного» вызова) может показаться вам беспринципным перекладыванием ответственности. Вы даже можете усмотреть в этом нарушение принципа, описанного в [[Правило 18: Проектируйте интерфейсы так, что их легко было использовать правильно и трудно – неправильно | правиле 18]]: интерфейс должно быть легко использовать правильно. На самом деле все не так. Если операция может завершиться неудачно с возбуждением исключения и есть необходимость обработать это исключение, то исключение должно возбуждаться <EM>функцией, не являющейся деструктором.</EM> Связано это с тем, что деструкторы, возбуждающие исключения, опасны и всегда чреваты преждевременным завершением программы или неопределенным поведением. Говоря клиентам, что они должны сами вызывать функцию close, мы не обременяем их лишней работой, а даем возможность обработать ошибки, на которые в противном случае они не смогли бы отреагировать. Если они считают, что им это ни к чему, то могут проигнорировать эту возможность, полагаясь на то, что соединение закроет деструктор DBConn. Если же при этом произойдет ошибка, то есть close возбудит исключение, то им не на что жаловаться, если DBConn проглотит его или прервет программу. В конце-то концов, у них ведь был случай отреагировать по-другому, а они им не воспользовались.</P> Что следует помнить *Деструкторы никогда не должны возбуждать исключений. Если функция, вызываемая в деструкторе, может это сделать, то деструктор обязан перехватывать все исключения, а затем «проглатывать» их либо прерывать программу. *Если клиенты класса нуждаются в возможности реагировать на исключения во время некоторой операции, то класс должен предоставить обычную функцию (то есть не деструктор), которая эту операцию выполнит. 759cc623e952db248a123deaea0e59dc6a49624e 29 28 2013-06-10T07:58:07Z Lerom 3360334 wikitext text/x-wiki <P>C++ не запрещает использовать исключения в деструкторах, но это, безусловно, очень нежелательная практика. На то есть серьезная причина. Рассмотрим пример:</P> <source lang="cpp"> class Widget { public: ... ~Widget() {...} // предположим, здесь есть исключение }; void doSomething() { std::vector<Widget> v; ... } // здесь v автоматически уничтожается </source> <P>Когда вектор v уничтожается, он отвечает за уничтожение всех объектов Widget, которые в нем содержатся. Предположим, что v содержит 10 объектов Widget, и во время уничтожения первого из них возбужается исключение. Остальные девять объектов Widget также должны быть уничтожены (иначе ресурсы, выделенные для них, будут потеряны), поэтому необходимо вызвать и их деструкторы. Но представим, что в это время деструктор второго объекта Widget также возбудит исключение. Тогда возникнет сразу два одновременно активных исключения, а это слишком много для C++. В зависимости от конкретных условий исполнение программы либо будет прервано, либо ее поведение окажется неопределенным. В этом примере как раз имеет место второй случай. И так будет происходить при использовании любого библиотечного контейнера (например, list, set), любого контейнера TR1 ([[Правило 54: Ознакомьтесь со стандартной библиотекой, включая TR1 | см. правило 54]]) и даже массива. И причина этой проблемы не в контейнерах или массивах. Преждевременное завершение программы или неопределенное поведение здесь является результатом того, что деструкторы возбуждают исключения. C++ <EM>не</EM> любит деструкторов, возбуждающих исключения!</P> <P>Это достаточно просто понять. Но что вы должны делать, если в вашем деструкторе необходимо выполнить операцию, которая может породить исключение? Например, предположим, что мы имеем дело с классом, описывающим подключение к базе данных:</P> <source lang="cpp"> class DBConnection { public: ... static DBConnection create(); // функция возвращает объект // DBConnection; параметры для // простоты опущены void close(); // закрыть соединение; при неудаче }; // возбуждает исключение </source> <P>Для гарантии того, что клиент не забудет вызвать close для объектов DBConnection, резонно создать класс для управления ресурсами DBConnection, который вызывает close в своем деструкторе. Классы, управляющие ресурсами, мы подробно рассмотрим в главе 3, а здесь достаточно прикинуть, как должен выглядеть деструктор такого класса:</P> <source lang="cpp"> class DBConn { // Класс для управления объектами public: // DBConnection ... ~DBConn() // обеспечить, чтобы соединения с базой { // данных всегда закрывались db.close(); } private: DBConnecton db; }; </source> <P>Тогда клиент может содержать такой код:</P> <source lang="cpp"> { // блок открывается DBConn dbc(DBConnection::create()); // создать объект DBConnection // и передать его объекту DBConn ... // использовать объект DBConnection // через интерфейс DBConn } // в конце блока объект DBConn // уничтожается, при этом // автоматически вызывается метод close // объекта DBConnection </source> <P>Все это приемлемо до тех пор, пока метод close завершается успешно, но если его вызов возбуждает исключение, то оно покидает пределы деструктора DBConn. Это очень плохо, потому что деструкторы, возбуждающие исключения, могут стать источниками ошибок.</P> <P>Есть два основных способа избежать этой проблемы. Деструктор DBConn может:</P> *<STRONG>Прервать программу,</STRONG> если close возбуждает исключение; обычно для этого вызывается функция abort: <source lang="cpp"> DBConn::~DBConn() { try {db.close();} catch(...) { //записать в протокол, что вызов close завершился неудачно; std::abort(); } } </source> <P>Это резонный выбор, если программа не может продолжать работу после того, как в деструкторе произошла ошибка. Преимущество такого подхода – в предотвращении неопределенного поведения. Вызов abort упредит возникновение неопределенности.</P> *<STRONG>Перехватить исключение,</STRONG> возбужденное вызовом close: <source lang="cpp"> DBConn::~DBConn() { try {db.close();} catch(...) { //записать в протокол, что вызов close завершился неудачно; } } </source> <P>Вообще говоря, такое «проглатывание» исключений – плохая идея, потому что мы теряем важную информацию: <EM>что-то не сработало</EM> ! Но иногда лучше поступить так, чтобы избежать преждевременной остановки программы или неопределенного поведения. Выбирать этот подход следует лишь в случае, когда программа в состоянии надежно продолжать исполнение, даже после того, как ошибка произошла, но была проигнорирована.</P> <P>Ни одно из этих решений не является идеальным. Проблема в том, что в обоих случаях программа не имеет возможности отреагировать на ситуацию, которая привела к возбуждению исключения внутри close.</P> <P>Более разумная стратегия – спроектировать интерфейс DBConn так, чтобы его клиенты сами имели возможность реагировать на возникающие ошибки. Например, класс DBConn может предоставить собственную функцию close и таким образом дать клиентам шанс обработать исключение, возникшее в процессе операции. Объект этого класса мог бы отслеживать, было ли соединение DBConnection уже закрыто функцией close, и, если это не так, закрывать его в деструкторе. Тем самым предотвращается утечка соединений. Но если close все-таки будет вызвана из деструктора и возбудит исключение, то мы опять возвращаемся к описанным выше вариантам: прервать программу или «проглотить» исключение:</P> <source lang="cpp"> class DBConn { public: ... void close() // новая функция для использования клиентом { db.close() closed = true; } ~DBConn() { if(!closed) try { db.close(); // закрыть соединение, если этого не сделал } // клиент catch(...) { // если возникнет исключение, запротоколировать записать в протокол, // и прервать программу или «проглотить» его что вызов close завершился неудачно; } } private: DBConnecton db; bool closed; }; </source> <P>Перемещение вызова close из деструктора DBConn в код клиента (и оставлением в деструкторе DBConn «страховочного» вызова) может показаться вам беспринципным перекладыванием ответственности. Вы даже можете усмотреть в этом нарушение принципа, описанного в [[Правило 18: Проектируйте интерфейсы так, что их легко было использовать правильно и трудно – неправильно | правиле 18]]: интерфейс должно быть легко использовать правильно. На самом деле все не так. Если операция может завершиться неудачно с возбуждением исключения и есть необходимость обработать это исключение, то исключение должно возбуждаться <EM>функцией, не являющейся деструктором.</EM> Связано это с тем, что деструкторы, возбуждающие исключения, опасны и всегда чреваты преждевременным завершением программы или неопределенным поведением. Говоря клиентам, что они должны сами вызывать функцию close, мы не обременяем их лишней работой, а даем возможность обработать ошибки, на которые в противном случае они не смогли бы отреагировать. Если они считают, что им это ни к чему, то могут проигнорировать эту возможность, полагаясь на то, что соединение закроет деструктор DBConn. Если же при этом произойдет ошибка, то есть close возбудит исключение, то им не на что жаловаться, если DBConn проглотит его или прервет программу. В конце-то концов, у них ведь был случай отреагировать по-другому, а они им не воспользовались.</P> == Что следует помнить == *Деструкторы никогда не должны возбуждать исключений. Если функция, вызываемая в деструкторе, может это сделать, то деструктор обязан перехватывать все исключения, а затем «проглатывать» их либо прерывать программу. *Если клиенты класса нуждаются в возможности реагировать на исключения во время некоторой операции, то класс должен предоставить обычную функцию (то есть не деструктор), которая эту операцию выполнит. 4372fe17661cf97704e2395ff479cdcd243de708 Правило 9: Никогда не вызывайте виртуальные функции в конструкторе или деструкторе 0 12 30 2013-06-13T05:09:18Z Lerom 3360334 Новая страница: «<P>Начну с повторения: вы не должны вызывать виртуальные функции во время работы конструк…» wikitext text/x-wiki <P>Начну с повторения: вы не должны вызывать виртуальные функции во время работы конструкторов или деструкторов, потому что эти вызовы будут делать не то, что вы думаете, и результатами их работы вы будете недовольны. Если вы – программист на Java или C#, то обратите на это правило особое внимание, потому что это в этом отношении C++ ведет себя иначе.</P> <P>Предположим, что имеется иерархия классов для моделирования биржевых транзакций, то есть поручений на покупку, на продажу и т. д. Важно, чтобы эти транзакции было легко проверить, поэтому каждый раз, когда создается новый объект транзакции, в протокол аудита должна вноситься соответствующая запись. Следующий подход к решению данной проблемы выглядит разумным:</P> <source lang="cpp"> class Transaction { // базовый класс для всех public: // транзакций Transaction(); virtual void logTransaction() const = 0; // выполняет зависящую от типа // запись в протокол ... }; Transaction::Transaction() // реализация конструктора { // базового класса ... logTransaction(); } class BuyTransaction: public Transaction { // производный класс public: virtual void logTransaction() const = 0; // как протоколировать // транзакции данного типа ... }; class SellTransaction: public Transaction { // производный класс public: virtual void logTransaction() const = 0; // как протоколировать // транзакции данного типа ... }; </source> <P>Посмотрим, что произойдет при исполнении следующего кода:</P> <source lang="cpp"> BuyTransaction b; </source> <P>Ясно, что будет вызван конструктор BuyTransaction, но сначала должен быть вызван конструктор Transaction, потому что части объекта, принадлежащие базовому классу, конструируются прежде, чем части, принадлежащие производному классу. В последней строке конструктора Transaction вызывается виртуальная функция logTransaction, тут-то и начинаются сюрпризы. Здесь вызывается та версия logTransaction, которая определена в классе Transaction, а не в BuyTransaction, несмотря на то что тип создаваемого объекта – BuyTransaction. Во время конструирования базового класса не вызываются виртуальные функции, определенные в производном классе. Объект ведет себя так, как будто он принадлежит базовому типу. Короче говоря, во время конструирования базового класса виртуальных функций не существует.</P> <P>Есть веская причина для столь, казалось бы, неожиданного поведения. Поскольку конструкторы базовых классов вызываются раньше, чем конструкторы производных, то данные-члены производного класса еще не инициализированы во время работы конструктора базового класса. Это может стать причиной неопределенного поведения и близкого знакомства с отладчиком. Обращение к тем частям объекта, которые еще не были инициализированы, опасно, поэтому C++ не дает такой возможности.</P> <P>Есть даже более фундаментальные причины. Пока над созданием объекта производного класса трудится конструктор базового класса, типом объекта <EM>является</EM> базовый класс. Не только виртуальные функции считают его таковым, но и все прочие механизмы языка, использующие информацию о типе во время исполнения (например, описанный в [[Правило 27: Не злоупотребляйте приведением типов | правиле 27]] оператор dynamic_cast и оператор typeid). В нашем примере, пока работает конструктор Transaction, инициализируя базовую часть объекта BuyTransaction, этот объект относится к типу Transaction. Именно так его воспринимают все части C++, и в этом есть смысл: части объекта, относящиеся к BuyTransaction, еще не инициализированы, поэтому безопаснее считать, что их не существует вовсе. Объект не является объектом производного класса до тех пор, пока не начнется исполнение конструктора последнего.</P> <P>То же относится и к деструкторам. Как только начинает исполнение деструктор производного класса, предполагается, что данные-члены, принадлежащие этому классу, не определены, поэтому C++ считает, что их больше не существует. При входе в деструктор базового класса наш объект становится объектом базового класса, и все части C++ – виртуальные функции, оператор dynamic_cast и т. п. – воспринимают его именно так.</P> <P>В приведенном выше примере кода конструктор Transaction напрямую обращается к виртуальной функции, что представляет собой откровенное нарушение принципов, описанных в данном правиле. Это нарушение легко обнаружить, поэтому некоторые компиляторы выдают предупреждение (а другие – нет; дискуссию о предупреждениях см. в [[Правило 53: Обращайте внимание на предупреждения компилятора | правиле 53]]). Но даже без такого предупреждения ошибка наверняка проявится до времени исполнения, потому что функция logTransaction в классе Transaction объявлена чисто виртуальной. Если только она не была где-то определена (маловероятно, но возможно – [[Правило 34: Различайте наследование интерфейса и наследование реализации | см. правило 34]]), то такая программа не скомпонуется: компоновщик не найдет необходимую реализацию Transaction::logTransaction.</P> <P>Не всегда так просто обнаружить вызов виртуальной функции во время работы конструктора или деструктора. Если Transaction имеет несколько конструкторов, каждый из которых выполняет одну и ту же работу, то следует проектировать программу так, чтобы избежать дублирования кода, поместив общую часть инициализации, включая вызов logTransaction, в закрытую невиртуальную функцию инициализации, скажем, init:</P> <BR><P><CODE>class Transaction {</CODE></P> <P><CODE>public:</CODE></P> <P><CODE>Transaction()</CODE></P> <P><CODE>{ init(); } // вызов невиртуальной функции</CODE></P> <P><CODE>Virtual void logTransaction() const = 0;</CODE></P> <P><CODE>...</CODE></P> <P><CODE>private:</CODE></P> <P><CODE>void init()</CODE></P> <P><CODE>{</CODE></P> <P><CODE>...</CODE></P> <P><CODE>logTransaction(); // а это вызов виртуальной</CODE></P> <P><CODE>// функции!</CODE></P> <P><CODE>}</CODE></P> <P><CODE>};</CODE></P> <BR><P>Концептуально этот код не отличается от приведенного выше, но он более коварный, потому что обычно будет скомпилирован и скомпонован без предупреждений. В этом случае, поскольку logTransaction – чисто виртуальная функция класса Transaction, в момент ее вызова большинство систем времени исполнения прервут программу (обычно выдав соответствующее сообщение). Однако если logTransaction будет «нормальной» виртуальной функцией, у которой в классе Transaction есть реализация, то эта функция и будет вызвана, и программа радостно продолжит работу, оставляя вас в недоумении, почему при создании объекта производного класса была вызвана неверная версия logTransaction. Единственный способ избежать этой проблемы – убедиться, что ни один из конструкторов и деструкторов не вызывает виртуальных функций при создании или уничтожении объекта, и что все функции, к которым они обращаются, следуют тому же правилу.</P> <P>Но как вы можете убедиться в том, что вызывается правильная версия log-Transaction при создании любого объекта из иерархии Transaction? Понятно, что вызов виртуальной функции объекта из конструкторов не годится.</P> <P>Есть разные варианты решения этой проблемы. Один из них – сделать функцию logTransaction невиртуальной в классе Transaction, затем потребовать, чтобы конструкторы производного класса передавали необходимую для записи в протокол информацию конструктору Transaction. Эта функция затем могла бы безопасно вызвать невиртуальную logTransaction. Примерно так:</P> <BR><P><CODE>class Transaction {</CODE></P> <P><CODE>public:</CODE></P> <P><CODE>explicit Transaction(const std::string&amp; loginfo);</CODE></P> <P><CODE>void logTransaction(const std::string&amp; loginfo) const; // теперь –</CODE></P> <P><CODE>// невиртуальная</CODE></P> <P><CODE>// функция</CODE></P> <P><CODE>...</CODE></P> <P><CODE>};</CODE></P> <P><CODE>Transaction::Transaction(const std::string&amp; loginfo)</CODE></P> <P><CODE>{</CODE></P> <P><CODE>...</CODE></P> <P><CODE>logTransaction(loginfo); // теперь –</CODE></P> <P><CODE>// невиртуальный</CODE></P> <P><CODE>// вызов</CODE></P> <P><CODE>}</CODE></P> <P><CODE>class BuyTransaction : public Transaction {</CODE></P> <P><CODE>public:</CODE></P> <P><CODE>BuyTransaction( <EM>parameters </EM>)</CODE></P> <P><CODE>: Transaction(createLogString( <EM>parameters </EM>)) // передать информацию</CODE></P> <P><CODE>{...} // для записи в протокол</CODE></P> <P><CODE>... // конструктору базового</CODE></P> <P><CODE>// класса</CODE></P> <P><CODE>private:</CODE></P> <P><CODE>static std::string createLogString( <EM>parameters </EM>);</CODE></P> <P><CODE>}</CODE></P> <BR><P>Другими словами, если вы не можете вызывать виртуальные функции из конструктора базового класса, то можете компенсировать это передачей необходимой информации конструктору базового класса из конструктора производного.</P> <P>В этом примере обратите внимание на применение закрытой статической функции createLogString в BuyTransaction. Использование вспомогательной функции для создания значения, передаваемого конструктору базового класса, часто удобнее (и лучше читается), чем отслеживание длинного списка инициализации членов для передачи базовому классу того, что ему нужно. Сделав эту функцию статической, мы избегаем опасности нечаянно сослаться на неинициализированные данные-члены класса BuyTransaction. Это важно, поскольку тот факт, что эти данные-члены еще не определены, и является основной причиной, почему нельзя вызывать виртуальные функции из конструкторов и деструкторов.</P> <H2><STRONG><EM><a name=label32 style="border:none;"></a>Что следует помнить</EM></STRONG></H2><P>• Не вызывайте виртуальные функции во время работы конструкторов и деструкторов, потому что такие вызовы никогда не дойдут до производных классов, расположенных в иерархии наследования ниже того, который сейчас конструируется или уничтожается.</P> b8a8697e1ac8da19c0369708154116ed3cb5f25c 31 30 2013-06-13T05:38:51Z Lerom 3360334 wikitext text/x-wiki <P>Начну с повторения: вы не должны вызывать виртуальные функции во время работы конструкторов или деструкторов, потому что эти вызовы будут делать не то, что вы думаете, и результатами их работы вы будете недовольны. Если вы – программист на Java или C#, то обратите на это правило особое внимание, потому что это в этом отношении C++ ведет себя иначе.</P> <P>Предположим, что имеется иерархия классов для моделирования биржевых транзакций, то есть поручений на покупку, на продажу и т. д. Важно, чтобы эти транзакции было легко проверить, поэтому каждый раз, когда создается новый объект транзакции, в протокол аудита должна вноситься соответствующая запись. Следующий подход к решению данной проблемы выглядит разумным:</P> <source lang="cpp"> class Transaction { // базовый класс для всех public: // транзакций Transaction(); virtual void logTransaction() const = 0; // выполняет зависящую от типа // запись в протокол ... }; Transaction::Transaction() // реализация конструктора { // базового класса ... logTransaction(); } class BuyTransaction: public Transaction { // производный класс public: virtual void logTransaction() const = 0; // как протоколировать // транзакции данного типа ... }; class SellTransaction: public Transaction { // производный класс public: virtual void logTransaction() const = 0; // как протоколировать // транзакции данного типа ... }; </source> <P>Посмотрим, что произойдет при исполнении следующего кода:</P> <source lang="cpp"> BuyTransaction b; </source> <P>Ясно, что будет вызван конструктор BuyTransaction, но сначала должен быть вызван конструктор Transaction, потому что части объекта, принадлежащие базовому классу, конструируются прежде, чем части, принадлежащие производному классу. В последней строке конструктора Transaction вызывается виртуальная функция logTransaction, тут-то и начинаются сюрпризы. Здесь вызывается та версия logTransaction, которая определена в классе Transaction, а не в BuyTransaction, несмотря на то что тип создаваемого объекта – BuyTransaction. Во время конструирования базового класса не вызываются виртуальные функции, определенные в производном классе. Объект ведет себя так, как будто он принадлежит базовому типу. Короче говоря, во время конструирования базового класса виртуальных функций не существует.</P> <P>Есть веская причина для столь, казалось бы, неожиданного поведения. Поскольку конструкторы базовых классов вызываются раньше, чем конструкторы производных, то данные-члены производного класса еще не инициализированы во время работы конструктора базового класса. Это может стать причиной неопределенного поведения и близкого знакомства с отладчиком. Обращение к тем частям объекта, которые еще не были инициализированы, опасно, поэтому C++ не дает такой возможности.</P> <P>Есть даже более фундаментальные причины. Пока над созданием объекта производного класса трудится конструктор базового класса, типом объекта <EM>является</EM> базовый класс. Не только виртуальные функции считают его таковым, но и все прочие механизмы языка, использующие информацию о типе во время исполнения (например, описанный в [[Правило 27: Не злоупотребляйте приведением типов | правиле 27]] оператор dynamic_cast и оператор typeid). В нашем примере, пока работает конструктор Transaction, инициализируя базовую часть объекта BuyTransaction, этот объект относится к типу Transaction. Именно так его воспринимают все части C++, и в этом есть смысл: части объекта, относящиеся к BuyTransaction, еще не инициализированы, поэтому безопаснее считать, что их не существует вовсе. Объект не является объектом производного класса до тех пор, пока не начнется исполнение конструктора последнего.</P> <P>То же относится и к деструкторам. Как только начинает исполнение деструктор производного класса, предполагается, что данные-члены, принадлежащие этому классу, не определены, поэтому C++ считает, что их больше не существует. При входе в деструктор базового класса наш объект становится объектом базового класса, и все части C++ – виртуальные функции, оператор dynamic_cast и т. п. – воспринимают его именно так.</P> <P>В приведенном выше примере кода конструктор Transaction напрямую обращается к виртуальной функции, что представляет собой откровенное нарушение принципов, описанных в данном правиле. Это нарушение легко обнаружить, поэтому некоторые компиляторы выдают предупреждение (а другие – нет; дискуссию о предупреждениях см. в [[Правило 53: Обращайте внимание на предупреждения компилятора | правиле 53]]). Но даже без такого предупреждения ошибка наверняка проявится до времени исполнения, потому что функция logTransaction в классе Transaction объявлена чисто виртуальной. Если только она не была где-то определена (маловероятно, но возможно – [[Правило 34: Различайте наследование интерфейса и наследование реализации | см. правило 34]]), то такая программа не скомпонуется: компоновщик не найдет необходимую реализацию Transaction::logTransaction.</P> <P>Не всегда так просто обнаружить вызов виртуальной функции во время работы конструктора или деструктора. Если Transaction имеет несколько конструкторов, каждый из которых выполняет одну и ту же работу, то следует проектировать программу так, чтобы избежать дублирования кода, поместив общую часть инициализации, включая вызов logTransaction, в закрытую невиртуальную функцию инициализации, скажем, init:</P> <source lang="cpp"> class Transaction { public: Transaction() { init(); } // вызов невиртуальной функции virtual void logTransaction() const = 0; ... private: void init() { ... logTransaction(); // а это вызов виртуальной // функции! } }; </source> <P>Концептуально этот код не отличается от приведенного выше, но он более коварный, потому что обычно будет скомпилирован и скомпонован без предупреждений. В этом случае, поскольку logTransaction – чисто виртуальная функция класса Transaction, в момент ее вызова большинство систем времени исполнения прервут программу (обычно выдав соответствующее сообщение). Однако если logTransaction будет «нормальной» виртуальной функцией, у которой в классе Transaction есть реализация, то эта функция и будет вызвана, и программа радостно продолжит работу, оставляя вас в недоумении, почему при создании объекта производного класса была вызвана неверная версия logTransaction. Единственный способ избежать этой проблемы – убедиться, что ни один из конструкторов и деструкторов не вызывает виртуальных функций при создании или уничтожении объекта, и что все функции, к которым они обращаются, следуют тому же правилу.</P> <P>Но как вы можете убедиться в том, что вызывается правильная версия log-Transaction при создании любого объекта из иерархии Transaction? Понятно, что вызов виртуальной функции объекта из конструкторов не годится.</P> <P>Есть разные варианты решения этой проблемы. Один из них – сделать функцию logTransaction невиртуальной в классе Transaction, затем потребовать, чтобы конструкторы производного класса передавали необходимую для записи в протокол информацию конструктору Transaction. Эта функция затем могла бы безопасно вызвать невиртуальную logTransaction. Примерно так:</P> <source lang="cpp"> class Transaction { public: explicit Transaction(const std::string& loginfo); void logTransaction(const std::string& loginfo) const; // теперь – // невиртуальная // функция ... }; Transaction::Transaction(const std::string& loginfo) { ... logTransaction(loginfo); // теперь – // невиртуальный // вызов } class BuyTransaction : public Transaction { public: BuyTransaction( parameters ) : Transaction(createLogString( parameters )) // передать информацию {...} // для записи в протокол ... // конструктору базового // класса private: static std::string createLogString( parameters ); } </source> <P>Другими словами, если вы не можете вызывать виртуальные функции из конструктора базового класса, то можете компенсировать это передачей необходимой информации конструктору базового класса из конструктора производного.</P> <P>В этом примере обратите внимание на применение закрытой статической функции createLogString в BuyTransaction. Использование вспомогательной функции для создания значения, передаваемого конструктору базового класса, часто удобнее (и лучше читается), чем отслеживание длинного списка инициализации членов для передачи базовому классу того, что ему нужно. Сделав эту функцию статической, мы избегаем опасности нечаянно сослаться на неинициализированные данные-члены класса BuyTransaction. Это важно, поскольку тот факт, что эти данные-члены еще не определены, и является основной причиной, почему нельзя вызывать виртуальные функции из конструкторов и деструкторов.</P> [[Что следует помнить]] *Не вызывайте виртуальные функции во время работы конструкторов и деструкторов, потому что такие вызовы никогда не дойдут до производных классов, расположенных в иерархии наследования ниже того, который сейчас конструируется или уничтожается. a14516b1f3ddccf0f30b52f7013c6827648aaf95 32 31 2013-06-13T05:39:38Z Lerom 3360334 wikitext text/x-wiki <P>Начну с повторения: вы не должны вызывать виртуальные функции во время работы конструкторов или деструкторов, потому что эти вызовы будут делать не то, что вы думаете, и результатами их работы вы будете недовольны. Если вы – программист на Java или C#, то обратите на это правило особое внимание, потому что это в этом отношении C++ ведет себя иначе.</P> <P>Предположим, что имеется иерархия классов для моделирования биржевых транзакций, то есть поручений на покупку, на продажу и т. д. Важно, чтобы эти транзакции было легко проверить, поэтому каждый раз, когда создается новый объект транзакции, в протокол аудита должна вноситься соответствующая запись. Следующий подход к решению данной проблемы выглядит разумным:</P> <source lang="cpp"> class Transaction { // базовый класс для всех public: // транзакций Transaction(); virtual void logTransaction() const = 0; // выполняет зависящую от типа // запись в протокол ... }; Transaction::Transaction() // реализация конструктора { // базового класса ... logTransaction(); } class BuyTransaction: public Transaction { // производный класс public: virtual void logTransaction() const = 0; // как протоколировать // транзакции данного типа ... }; class SellTransaction: public Transaction { // производный класс public: virtual void logTransaction() const = 0; // как протоколировать // транзакции данного типа ... }; </source> <P>Посмотрим, что произойдет при исполнении следующего кода:</P> <source lang="cpp"> BuyTransaction b; </source> <P>Ясно, что будет вызван конструктор BuyTransaction, но сначала должен быть вызван конструктор Transaction, потому что части объекта, принадлежащие базовому классу, конструируются прежде, чем части, принадлежащие производному классу. В последней строке конструктора Transaction вызывается виртуальная функция logTransaction, тут-то и начинаются сюрпризы. Здесь вызывается та версия logTransaction, которая определена в классе Transaction, а не в BuyTransaction, несмотря на то что тип создаваемого объекта – BuyTransaction. Во время конструирования базового класса не вызываются виртуальные функции, определенные в производном классе. Объект ведет себя так, как будто он принадлежит базовому типу. Короче говоря, во время конструирования базового класса виртуальных функций не существует.</P> <P>Есть веская причина для столь, казалось бы, неожиданного поведения. Поскольку конструкторы базовых классов вызываются раньше, чем конструкторы производных, то данные-члены производного класса еще не инициализированы во время работы конструктора базового класса. Это может стать причиной неопределенного поведения и близкого знакомства с отладчиком. Обращение к тем частям объекта, которые еще не были инициализированы, опасно, поэтому C++ не дает такой возможности.</P> <P>Есть даже более фундаментальные причины. Пока над созданием объекта производного класса трудится конструктор базового класса, типом объекта <EM>является</EM> базовый класс. Не только виртуальные функции считают его таковым, но и все прочие механизмы языка, использующие информацию о типе во время исполнения (например, описанный в [[Правило 27: Не злоупотребляйте приведением типов | правиле 27]] оператор dynamic_cast и оператор typeid). В нашем примере, пока работает конструктор Transaction, инициализируя базовую часть объекта BuyTransaction, этот объект относится к типу Transaction. Именно так его воспринимают все части C++, и в этом есть смысл: части объекта, относящиеся к BuyTransaction, еще не инициализированы, поэтому безопаснее считать, что их не существует вовсе. Объект не является объектом производного класса до тех пор, пока не начнется исполнение конструктора последнего.</P> <P>То же относится и к деструкторам. Как только начинает исполнение деструктор производного класса, предполагается, что данные-члены, принадлежащие этому классу, не определены, поэтому C++ считает, что их больше не существует. При входе в деструктор базового класса наш объект становится объектом базового класса, и все части C++ – виртуальные функции, оператор dynamic_cast и т. п. – воспринимают его именно так.</P> <P>В приведенном выше примере кода конструктор Transaction напрямую обращается к виртуальной функции, что представляет собой откровенное нарушение принципов, описанных в данном правиле. Это нарушение легко обнаружить, поэтому некоторые компиляторы выдают предупреждение (а другие – нет; дискуссию о предупреждениях см. в [[Правило 53: Обращайте внимание на предупреждения компилятора | правиле 53]]). Но даже без такого предупреждения ошибка наверняка проявится до времени исполнения, потому что функция logTransaction в классе Transaction объявлена чисто виртуальной. Если только она не была где-то определена (маловероятно, но возможно – [[Правило 34: Различайте наследование интерфейса и наследование реализации | см. правило 34]]), то такая программа не скомпонуется: компоновщик не найдет необходимую реализацию Transaction::logTransaction.</P> <P>Не всегда так просто обнаружить вызов виртуальной функции во время работы конструктора или деструктора. Если Transaction имеет несколько конструкторов, каждый из которых выполняет одну и ту же работу, то следует проектировать программу так, чтобы избежать дублирования кода, поместив общую часть инициализации, включая вызов logTransaction, в закрытую невиртуальную функцию инициализации, скажем, init:</P> <source lang="cpp"> class Transaction { public: Transaction() { init(); } // вызов невиртуальной функции virtual void logTransaction() const = 0; ... private: void init() { ... logTransaction(); // а это вызов виртуальной // функции! } }; </source> <P>Концептуально этот код не отличается от приведенного выше, но он более коварный, потому что обычно будет скомпилирован и скомпонован без предупреждений. В этом случае, поскольку logTransaction – чисто виртуальная функция класса Transaction, в момент ее вызова большинство систем времени исполнения прервут программу (обычно выдав соответствующее сообщение). Однако если logTransaction будет «нормальной» виртуальной функцией, у которой в классе Transaction есть реализация, то эта функция и будет вызвана, и программа радостно продолжит работу, оставляя вас в недоумении, почему при создании объекта производного класса была вызвана неверная версия logTransaction. Единственный способ избежать этой проблемы – убедиться, что ни один из конструкторов и деструкторов не вызывает виртуальных функций при создании или уничтожении объекта, и что все функции, к которым они обращаются, следуют тому же правилу.</P> <P>Но как вы можете убедиться в том, что вызывается правильная версия log-Transaction при создании любого объекта из иерархии Transaction? Понятно, что вызов виртуальной функции объекта из конструкторов не годится.</P> <P>Есть разные варианты решения этой проблемы. Один из них – сделать функцию logTransaction невиртуальной в классе Transaction, затем потребовать, чтобы конструкторы производного класса передавали необходимую для записи в протокол информацию конструктору Transaction. Эта функция затем могла бы безопасно вызвать невиртуальную logTransaction. Примерно так:</P> <source lang="cpp"> class Transaction { public: explicit Transaction(const std::string& loginfo); void logTransaction(const std::string& loginfo) const; // теперь – // невиртуальная // функция ... }; Transaction::Transaction(const std::string& loginfo) { ... logTransaction(loginfo); // теперь – // невиртуальный // вызов } class BuyTransaction : public Transaction { public: BuyTransaction( parameters ) : Transaction(createLogString( parameters )) // передать информацию {...} // для записи в протокол ... // конструктору базового // класса private: static std::string createLogString( parameters ); } </source> <P>Другими словами, если вы не можете вызывать виртуальные функции из конструктора базового класса, то можете компенсировать это передачей необходимой информации конструктору базового класса из конструктора производного.</P> <P>В этом примере обратите внимание на применение закрытой статической функции createLogString в BuyTransaction. Использование вспомогательной функции для создания значения, передаваемого конструктору базового класса, часто удобнее (и лучше читается), чем отслеживание длинного списка инициализации членов для передачи базовому классу того, что ему нужно. Сделав эту функцию статической, мы избегаем опасности нечаянно сослаться на неинициализированные данные-члены класса BuyTransaction. Это важно, поскольку тот факт, что эти данные-члены еще не определены, и является основной причиной, почему нельзя вызывать виртуальные функции из конструкторов и деструкторов.</P> == Что следует помнить == *Не вызывайте виртуальные функции во время работы конструкторов и деструкторов, потому что такие вызовы никогда не дойдут до производных классов, расположенных в иерархии наследования ниже того, который сейчас конструируется или уничтожается. 92b86c7a47ea1b012ded42b37bf665bc4c677a80 Правило 10: Операторы присваивания должны возвращать ссылку на *this 0 13 33 2013-06-13T05:52:38Z Lerom 3360334 Новая страница: «<P>Одно из интересных свойств присваивания состоит в том, что такие операции можно выполн…» wikitext text/x-wiki <P>Одно из интересных свойств присваивания состоит в том, что такие операции можно выполнять последовательно:</P> <source lang="cpp"> int x,y,z; x = y = z = 15; // цепочка присваиваний </source> <P>Также интересно, что оператор присваивания правоассоциативен, поэтому приведенный выше пример присваивания интерпретируется следующим образом:</P> <source lang="cpp"> x = (y = (z = 15)); </source> <P>Здесь переменной z присваивается значение 15, затем результат присваивания (новое значение z) присваивается переменной y, после чего результат (новое значение y) присваивается переменной x.</P> <P>Достигается это за счет того, что оператор присваивания возвращает ссылку на свой левый аргумент, и этому соглашению вы должны следовать при реализации операторов присваивания в своих классах:</P> <source lang="cpp"> class Widget { public: ... Widget& operator=(const Widget& rhs) // возвращаемый тип – ссылка { // на текущий класс ... return *this; // вернуть объект из левой части } // выражения ... }; </source> <P>Это соглашение касается всех операторов присваивания, а не только стандартной формы, показанной выше. Следовательно:</P> <source lang="cpp"> class Widget { public: ... Widget& operator+=(const Widget& rhs) // соглашение распространяется на { // +=, -=, *=, и т. д. ... return *this; } Widget& operator=(int rhs) // это относится даже { // к параметрам разных типов ... return *this; } ... }; </source> <P>Это всего лишь соглашение. Если программа его не придерживается, она тем не менее скомпилируется. Однако ему следуют все встроенные типы, как и все типы ([[Правило 54: Ознакомьтесь со стандартной библиотекой, включая TR1 | см. правило 54]]) стандартной библиотеки (то есть string, vector, complex, tr1::shared_ptr и т. д.). Если у вас нет веской причины нарушать соглашение, не делайте этого.</P> == Что следует помнить == *Пишите операторы присваивания так, чтобы они возвращали ссылку на *this. 7dfad58f174b2b9caff72ea8e49e8d25de8fd4a9 Правило 11: В operator= осуществляйте проверку на присваивание самому себе 0 14 34 2013-06-13T06:16:01Z Lerom 3360334 Новая страница: «<P>Присваивание самому себе возникает примерно в такой ситуации:</P> <source lang="cpp"> class Widget {...}; …» wikitext text/x-wiki <P>Присваивание самому себе возникает примерно в такой ситуации:</P> <source lang="cpp"> class Widget {...}; Widget w; ... w = w; // присваивание себе </source> <P>Код выглядит достаточно нелепо, однако он совершенно корректен, и в том, что программисты на такое способны, вы можете не сомневаться ни секунды.</P> <P>Кроме того, присваивание самому себе не всегда так легко узнаваемо. Например:</P> <source lang="cpp"> a[i] = a[j]; // потенциальное присваивание себе </source> <P>это присваивание себе, если i и j равны одному и тому же значению, и</P> <source lang="cpp"> *px = *py; // потенциальное присваивание себе </source> <P>тоже становится присваиванием самому себе, если окажется, что px и py указывают на одно и то же.</P> <P>Эти менее очевидные случаи присваивания себе являются результатом совмещения имен <EM>(aliasing),</EM> когда для ссылки на объект существует более одного способа. Вообще, программа, которая оперирует ссылками или указателями на различные объекты одного и того же типа, должна считаться с тем, что эти объекты могут совпадать. Необязательно даже, чтобы два объекта имели одинаковый тип, ведь если они принадлежат к одной иерархии классов, то ссылка или указатель на базовый класс может в действительно относиться к объекту производного класса:</P> <source lang="cpp"> class Base {...}; class Derived: public Base {...}; void doSomething(const Base& rb, // rb и *pd могут быть одним и тем же Derived *pd); // объектом </source> <P>Если вы следуете правилам [[Правило 13: Используйте объекты для управления ресурсами | 13]] и [[Правило 14: Тщательно продумывайте поведение при копировании классов, управляющих ресурсами | 14]], то всегда пользуетесь объектами для управления ресурсами; следите за тем, чтобы управляющие объекты правильно вели себя при копировании. В таком случае операторы присваивания должны быть безопасны относительно присваивания самому себе. Если вы пытаетесь управлять ресурсами самостоятельно (а как же иначе, если вы пишете класс для управления ресурсами), то можете попасть в ловушку, нечаянно освободив ресурс до его использования. Например, предположим, что вы создали класс, который содержит указатель на динамически распределенный объект класса Bitmap:</P> <source lang="cpp"> class Bitmap {...}; class Widget { ... private: Bitmap *pb; // указатель на объект, размещенный в куче }; </source> <P>Ниже приведена реализация оператора присваивания operator=, которая выглядит совершенно нормально, но становится опасной в случае выполнения присваивания самому себе (она также небезопасна с точки зрения исключений, но сейчас не об этом).</P> <source lang="cpp"> Widget& Widget::operator=(const Widget& rhs) // небезопасная реализация operator= { delete pb; // прекратить использование текущего // объекта Bitmap pb = new Bitmap(*rhs.pb); // начать использование копии объекта // Bitmap, указанной в правой части return *this; // см. правило 10 } </source> <P>Проблема состоит в том, что внутри operator= *this (чему присваивается значение) и rhs (что присваивается) могут оказаться одним и тем же объектом. Если это случится, то delete уничтожит не только Bitmap, принадлежащий текущему объекту, но и Bitmap, принадлежащий объекту в правой части. По завершении работы этой функции Widget, который не должен был бы измениться в процессе присваивания самому себе, содержит указатель на удаленный объект!</P> <P>Традиционный способ предотвратить эту ошибку состоит в том, что нужно выполнить проверку совпадения в начале operator=:</P> <source lang="cpp"> Widget& Widget::operator=(const Widget& rhs) // небезопасная реализация operator= { if(this == &rhs) return *this; // проверка совпадения: если // присваивание самому себе, то // ничего не делать delete pb; pb = new Bitmap(*rhs.pb); return *this; } </source> <P>Это решает проблему, но я уже упоминал, что предыдущая версия оператора присваивания была не только опасна в случае присваивания себе, но и небезопасна в смысле исключений, и последняя опасность остается актуальной во второй версии. В частности, если выражение «new Bitmap» вызовет исключение (либо по причине недостатка свободной памяти, либо исключение возбудит конструктор копирования Bitmap), то Widget также будет содержать указатель на несуществующий Bitmap. Такие указатели – источник неприятностей. Их нельзя безопасно удалить, их даже нельзя разыменовывать. А вот потратить массу времени на отладку, выясняя, откуда они взялись, – это можно.</P> <P>К счастью, существует способ одновременно сделать operator= безопасным в смысле исключений и безопасным по части присваивания самому себе. Поэтому все чаще программисты не занимаются специально присваиванием самому себе, а сосредоточивают усилия на достижении безопасности в смысле исключений. В [[Правило 29: Стремитесь, чтобы программа была безопасна относительно исключений | правиле 29]] эта проблема рассмотрена детально, а сейчас достаточно упомянуть, что во многих случаях продуманная последовательность операторов присваивания может обеспечить безопасность в смысле исключений (а заодно безопасность присваивания самому себе) кода. Например, ниже мы просто не удаляем pb до тех пор, пока не скопируем то, на что он указывает:</P> <source lang="cpp"> Widget& Widget::operator=(const Widget& rhs) { Bitmap *pOrig = pb; // запомнить исходный pb pb = new Bitmap(*rhs.pb); // установить указатель pb на копию *pb delete pOrig; // удалить исходный pb return *this; } </source> <P>Теперь, если «new Bitmap» возбудит исключение, то pb (и объект Widget, которому он принадлежит) останется неизменным. Даже без проверки на совпадение здесь обрабатывается присваивание самому себе, потому что мы сделали копию исходного объекта Bitmap, удалили его, а затем направили указатель на сделанную копию. Возможно, это не самый эффективный способ обработать присваивание самому себе, но он работает.</P> <P>Если вы печетесь об эффективности, то можете вернуть проверку на совпадение в начале функции. Но прежде спросите себя, как часто может происходить присваивание самому себе, потому что выполнение проверки тоже не обходится даром. Это делает код (исходный и объектный) чуть больше, а ветвление несколько снижает скорость исполнения. Эффективность предварительной выборки команд, кэширования и конвейеризации тоже может пострадать.</P> <P>Альтернативой ручному упорядочиванию предложений в operator= может быть обеспечение и безопасности в смысле исключений, и безопасности присваивания самому себе за счет применения техники «копирования с обменом» («copy and swap»). Она тесно связана с безопасностью в смысле исключений, поэтому рассматривается в [[Правило 29: Стремитесь, чтобы программа была безопасна относительно исключений | правиле 29]]. Тем не менее это достаточно распространенный способ написания operator=, и на него стоит взглянуть:</P> <source lang="cpp"> class Widget { ... void swap(Widget& rhs); // обмен данными *this и rhs ... // см. подробности в правиле 29 }; Widget& Widget:: operator=(const Widget& rhs) { Widget temp(rhs); // создать копию данных rhs swap(temp); // обменять данные *this с копией return *this; } </source> <P>Здесь мы пользуемся тем, что: (1) оператор присваивания можно объявить как принимающим аргумент по значению и (2) передача объекта по значению означает создание копии этого объекта ([[Правило 20: Предпочитайте передачу по ссылке на const передаче по значению | см. правило 20]]):</P> <source lang="cpp"> Widget& Widget::operator=(Widget rhs) // rhs – копия переданного объекта { // обратите внимание на передачу по // значению swap(rhs); // обменять данные *this с копией return *this; } </source> <P>Лично меня беспокоит, что такой подход приносит ясность в жертву изощренности, но, перемещая операцию копирования из тела функции в конструирование параметра, компилятор иногда может сгенерировать более эффективный код. == Что следует помнить == *Убедитесь, что operator= правильно ведет себя, когда объект присваивается самому себе. Для этого можно сравнить адреса исходного и целевого объектов, аккуратно упорядочить предложения или применить идиому копирования обменом. *Убедитесь, что все функции, оперирующие более чем одним объектом, ведут себя корректно при совпадении двух или более объектов. eca784e2f485955b9de6eb0307bb3c97eb214ede Правило 12: Копируйте все части объекта 0 15 35 2013-06-13T06:39:34Z Lerom 3360334 Новая страница: «<P>В хорошо спроектированных объектно-ориентированных системах, которые инкапсулируют в…» wikitext text/x-wiki <P>В хорошо спроектированных объектно-ориентированных системах, которые инкапсулируют внутреннее устройство объектов, копированием занимаются только две функции: конструктор копирования и оператор присваивания. Назовем их <EM>функциями копирования.</EM> В [[Правило 5: Какие функции C++ создает и вызывает молча | правиле 5]] я говорил, что компилятор генерирует копирующие функции при необходимости, и объяснял, что сгенерированные компилятором версии делают точно то, что вы ожидаете: копию всех данных исходного объекта.</P> <P>Объявляя собственные копирующие функции, вы сообщаете компилятору, что реализация по умолчанию вам чем-то не нравится. Компилятор «обижается» и мстит оригинальным образом: он не сообщает, если в вашей реализации что-то неправильно.</P> <P>Рассмотрим класс, представляющий заказчиков, в котором копирующие функции написаны вручную таким образом, что их вызовы протоколируются:</P> <source lang="cpp"> void logCall(const std::string& funcName); // делает запись в протокол class Customer { public: ... Customer(const Customer& rhs); Customer& operator=(const Customer& rhs); ... private: std::string name; }; Customer::Customer(const Customer& rhs) : name(rhs.name) // копировать данные rhs { logCall(“Конструктор копирования Customer”); } Customer& Customer::operator=(const Customer& rhs) { logCall(“Копирующий оператор присвоения Customer”); name = rhs.name; // копировать данные rhs return *this; // см. правило 10 } </source> <P>Все здесь выглядит отлично, и на самом деле так оно и есть – до тех пор, пока в класс Customer не будет добавлен новый член:</P> <source lang="cpp"> class Date {...}; // для даты и времени class Customer { public: //как раньше private: std::string name; Date lastTransaction; }; </source> <P>С этого момента существующие функции копирования копируют только часть объекта, именно поле name, но не поле lastTransaction. Однако большинство компиляторов ничего не скажут об этом даже при установке максимального уровня диагностики (см. также [[Правило 53: Обращайте внимание на предупреждения компилятора | правило 53]]). Вот к чему приводит самостоятельное написание функций копирования. Вы отвергаете функции, которые генерирует компилятор, поэтому он не сообщает, что ваш код не полон. Решение очевидно: если вы добавляете новый член в класс, то должны обновить и копирующие функции (а также все конструкторы [см. правила [[Правило 4: Прежде чем использовать объекты, убедитесь, что они инициализированы | 4]] и [[Правило 45: Разрабатывайте шаблоны функций-членов так, чтобы они принимали «все совместимые типы» | 45]]] и все нестандартные варианты operator= в классе [пример в [[Правило 10: Операторы присваивания должны возвращать ссылку на *this | правиле 10]]]; если вы забудете, то компилятор вряд ли напомнит).</P> <P>Одним из наиболее коварных случаев проявления этой ситуации является наследование. Рассмотрим пример:</P> <source lang="cpp"> class PriorityCustomer: public Customer { // производный класс public: ... PriorityCustomer(const PriorityCustomer& rhs); PriorityCustomer& operator=(const PriorityCustomer& rhs); ... private: int priority; }; PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) : priority(rhs.priority) { logCall(“Конструктор копирования PriorityCustomer”); } PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) { logCall(“Оператор присваивания PriorityCustomer”); priority = rhs. Priority; return *this; } </source> <P>На первый взгляд, копирующие функции в классе PriorityCustomer копируют все его члены, но приглядитесь внимательнее. Да, они копируют данные-члены, которые объявлены в PriorityCustomer, но каждый объект PriorityCustomer также содержит члены, унаследованные от Customer, а они-то не копируются вовсе! Конструктор копирования PriorityCustomer не специфицирует аргументы, которые должны быть переданы конструктору его базового класса (то есть не упоминает Customer в своем списке инициализации членов), поэтому часть Customer объекта PriorityCustomer будет инициализирована конструктором Customer, не принимающим аргументов, конструктором по умолчанию (если он отсутствует, то такой код просто не скомпилируется). Этот конструктор выполняет инициализацию по умолчанию членов name и lastTransaction.</P> <P>Для оператора присваивания PriorityCustomer ситуация мало чем отличается. Он не выполняет никаких попыток модифицировать данные-члены базового класса, поэтому они остаются неизменными.</P> <P>Всякий раз, когда вы самостоятельно пишете копирующие функции для производного класса, позаботьтесь о том, чтобы скопировать части базового класса. Обычно они находятся в закрытом разделе класса ([[Правило 22: Объявляйте данные-члены закрытыми | см. правило 22]]), поэтому у вас нет прямого доступа к ним. Поэтому копирующие функции производного класса должны вызывать соответствующие функции базового класса:</P> <source lang="cpp"> PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) : Customer(rhs), // вызвать копирующий конструктор // базового класса priority(rhs.priority) { logCall(“Конструктор копирования PriorityCustomer”); } PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) { logCall(“Оператор присваивания PriorityCustomer”); Customer::operator=(rhs); // присвоить значения данным-членам // базового класса priority = rhs. Priority; return *this; } </source> <P>Значение фразы «копировать все части» в заголовке этого параграфа теперь должно быть понятно. Когда вы пишете копирующие функции, убедитесь, что (1) копируются все локальные данные-члены и (2) вызываются соответствующие копирующие функции всех базовых классов.</P> <P>На практике эти две копирующие функции часто имеют похожие реализации, и у вас может возникнуть соблазн избежать дублирования кода за счет вызова одной функции из другой. Такое стремление похвально, но вызов одной копирующей функции из другой – неверный путь.</P> <P>Нет смысла вызывать конструктор копирования из оператора присваивания, поскольку вы тем самым попытаетесь сконструировать объект, который уже существует. Это настолько бессмысленно, что даже не существует синтаксиса для такой операции. Есть синтаксис, который выглядит так, будто вы делаете это, хотя на самом деле он означает совсем иное. Есть также синтаксис, который позволяет это сделать, но совершенно неочевидным способом, причем при некоторых условиях ваш объект может быть поврежден. Поэтому я не покажу ни тот, ни другой. Просто примите как данность, что вызывать из оператора присваивания конструктор копирования не следует.</P> <P>Попытка выполнить обратную операцию – из конструктора копирования вызвать оператор присваивания – также бессмысленна. Конструктор инициализирует новые объекты, а оператор присваивания работает с уже существующими и инициализированными объектами. Выполнять присваивание объекту, находящемуся в процессе конструирования, – значит делать с еще не инициализированным объектом что-то такое, что имеет смысл только для инициализированного объекта. Нонсенс! Даже не пытайтесь.</P> <P>Но если вы обнаружите, что ваш конструктор копирования и оператор присваивания содержат похожий код, попробуйте избежать дублирования, создав функцию-член, которую будут вызывать оба. Такая функция обычно делается закрытой и часто называется init. Эта стратегия представляет безопасный, испытанный способ избежать дублирования кода в конструкторах копирования и операторах присваивания.</P> Что следует помнить *Копирующие функции должны гарантировать копирование всех членов-данных объекта и частей его базовых классов. *Не пытайтесь реализовать одну из копирующих функций в терминах другой. Вместо этого поместите общую функциональность в третью функцию, которую вызовут обе. 592049d90bedecb5bba4d89e305c3234a3beda4e 36 35 2013-06-13T06:40:11Z Lerom 3360334 wikitext text/x-wiki <P>В хорошо спроектированных объектно-ориентированных системах, которые инкапсулируют внутреннее устройство объектов, копированием занимаются только две функции: конструктор копирования и оператор присваивания. Назовем их <EM>функциями копирования.</EM> В [[Правило 5: Какие функции C++ создает и вызывает молча | правиле 5]] я говорил, что компилятор генерирует копирующие функции при необходимости, и объяснял, что сгенерированные компилятором версии делают точно то, что вы ожидаете: копию всех данных исходного объекта.</P> <P>Объявляя собственные копирующие функции, вы сообщаете компилятору, что реализация по умолчанию вам чем-то не нравится. Компилятор «обижается» и мстит оригинальным образом: он не сообщает, если в вашей реализации что-то неправильно.</P> <P>Рассмотрим класс, представляющий заказчиков, в котором копирующие функции написаны вручную таким образом, что их вызовы протоколируются:</P> <source lang="cpp"> void logCall(const std::string& funcName); // делает запись в протокол class Customer { public: ... Customer(const Customer& rhs); Customer& operator=(const Customer& rhs); ... private: std::string name; }; Customer::Customer(const Customer& rhs) : name(rhs.name) // копировать данные rhs { logCall(“Конструктор копирования Customer”); } Customer& Customer::operator=(const Customer& rhs) { logCall(“Копирующий оператор присвоения Customer”); name = rhs.name; // копировать данные rhs return *this; // см. правило 10 } </source> <P>Все здесь выглядит отлично, и на самом деле так оно и есть – до тех пор, пока в класс Customer не будет добавлен новый член:</P> <source lang="cpp"> class Date {...}; // для даты и времени class Customer { public: //как раньше private: std::string name; Date lastTransaction; }; </source> <P>С этого момента существующие функции копирования копируют только часть объекта, именно поле name, но не поле lastTransaction. Однако большинство компиляторов ничего не скажут об этом даже при установке максимального уровня диагностики (см. также [[Правило 53: Обращайте внимание на предупреждения компилятора | правило 53]]). Вот к чему приводит самостоятельное написание функций копирования. Вы отвергаете функции, которые генерирует компилятор, поэтому он не сообщает, что ваш код не полон. Решение очевидно: если вы добавляете новый член в класс, то должны обновить и копирующие функции (а также все конструкторы [см. правила [[Правило 4: Прежде чем использовать объекты, убедитесь, что они инициализированы | 4]] и [[Правило 45: Разрабатывайте шаблоны функций-членов так, чтобы они принимали «все совместимые типы» | 45]]] и все нестандартные варианты operator= в классе [пример в [[Правило 10: Операторы присваивания должны возвращать ссылку на *this | правиле 10]]]; если вы забудете, то компилятор вряд ли напомнит).</P> <P>Одним из наиболее коварных случаев проявления этой ситуации является наследование. Рассмотрим пример:</P> <source lang="cpp"> class PriorityCustomer: public Customer { // производный класс public: ... PriorityCustomer(const PriorityCustomer& rhs); PriorityCustomer& operator=(const PriorityCustomer& rhs); ... private: int priority; }; PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) : priority(rhs.priority) { logCall(“Конструктор копирования PriorityCustomer”); } PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) { logCall(“Оператор присваивания PriorityCustomer”); priority = rhs. Priority; return *this; } </source> <P>На первый взгляд, копирующие функции в классе PriorityCustomer копируют все его члены, но приглядитесь внимательнее. Да, они копируют данные-члены, которые объявлены в PriorityCustomer, но каждый объект PriorityCustomer также содержит члены, унаследованные от Customer, а они-то не копируются вовсе! Конструктор копирования PriorityCustomer не специфицирует аргументы, которые должны быть переданы конструктору его базового класса (то есть не упоминает Customer в своем списке инициализации членов), поэтому часть Customer объекта PriorityCustomer будет инициализирована конструктором Customer, не принимающим аргументов, конструктором по умолчанию (если он отсутствует, то такой код просто не скомпилируется). Этот конструктор выполняет инициализацию по умолчанию членов name и lastTransaction.</P> <P>Для оператора присваивания PriorityCustomer ситуация мало чем отличается. Он не выполняет никаких попыток модифицировать данные-члены базового класса, поэтому они остаются неизменными.</P> <P>Всякий раз, когда вы самостоятельно пишете копирующие функции для производного класса, позаботьтесь о том, чтобы скопировать части базового класса. Обычно они находятся в закрытом разделе класса ([[Правило 22: Объявляйте данные-члены закрытыми | см. правило 22]]), поэтому у вас нет прямого доступа к ним. Поэтому копирующие функции производного класса должны вызывать соответствующие функции базового класса:</P> <source lang="cpp"> PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) : Customer(rhs), // вызвать копирующий конструктор // базового класса priority(rhs.priority) { logCall(“Конструктор копирования PriorityCustomer”); } PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) { logCall(“Оператор присваивания PriorityCustomer”); Customer::operator=(rhs); // присвоить значения данным-членам // базового класса priority = rhs. Priority; return *this; } </source> <P>Значение фразы «копировать все части» в заголовке этого параграфа теперь должно быть понятно. Когда вы пишете копирующие функции, убедитесь, что (1) копируются все локальные данные-члены и (2) вызываются соответствующие копирующие функции всех базовых классов.</P> <P>На практике эти две копирующие функции часто имеют похожие реализации, и у вас может возникнуть соблазн избежать дублирования кода за счет вызова одной функции из другой. Такое стремление похвально, но вызов одной копирующей функции из другой – неверный путь.</P> <P>Нет смысла вызывать конструктор копирования из оператора присваивания, поскольку вы тем самым попытаетесь сконструировать объект, который уже существует. Это настолько бессмысленно, что даже не существует синтаксиса для такой операции. Есть синтаксис, который выглядит так, будто вы делаете это, хотя на самом деле он означает совсем иное. Есть также синтаксис, который позволяет это сделать, но совершенно неочевидным способом, причем при некоторых условиях ваш объект может быть поврежден. Поэтому я не покажу ни тот, ни другой. Просто примите как данность, что вызывать из оператора присваивания конструктор копирования не следует.</P> <P>Попытка выполнить обратную операцию – из конструктора копирования вызвать оператор присваивания – также бессмысленна. Конструктор инициализирует новые объекты, а оператор присваивания работает с уже существующими и инициализированными объектами. Выполнять присваивание объекту, находящемуся в процессе конструирования, – значит делать с еще не инициализированным объектом что-то такое, что имеет смысл только для инициализированного объекта. Нонсенс! Даже не пытайтесь.</P> <P>Но если вы обнаружите, что ваш конструктор копирования и оператор присваивания содержат похожий код, попробуйте избежать дублирования, создав функцию-член, которую будут вызывать оба. Такая функция обычно делается закрытой и часто называется init. Эта стратегия представляет безопасный, испытанный способ избежать дублирования кода в конструкторах копирования и операторах присваивания.</P> == Что следует помнить == *Копирующие функции должны гарантировать копирование всех членов-данных объекта и частей его базовых классов. *Не пытайтесь реализовать одну из копирующих функций в терминах другой. Вместо этого поместите общую функциональность в третью функцию, которую вызовут обе. 4fdcf3e4ea54a73659fb428a9413193766c8bbc6 Правило 13: Используйте объекты для управления ресурсами 0 16 37 2013-06-17T04:58:12Z Lerom 3360334 Новая страница: «<P>Предположим, что мы работаем с библиотекой, моделирующей инвестиции (то есть акции, обл…» wikitext text/x-wiki <P>Предположим, что мы работаем с библиотекой, моделирующей инвестиции (то есть акции, облигации и т. п.), и классы, представляющие разные виды инвестиций, наследуются от корневого класса Investment:</P> <source lang="cpp"> class Investment {...} // корневой класс иерархии // типов инвестиций </source> <P>Предположим далее, что библиотека предоставляет объекты, описывающие конкретные инвестиции, с помощью фабричной функции ([[Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе | см. правило 7]]):</P> </P> <source lang="cpp"> Investment *createInvestment(); // возвращает указатель на динамически // распределенный объект в иерархии // Investment: вызвавший клиент обязан // удалить его (параметры для простоты // опущены) </source> <P>Как следует из комментария, пользователь, вызвавший createlnvestment, отвечает за удаление объекта, возвращенного этой функцией, по окончании его использования. Рассмотрим теперь функцию f, которая это делает:</P> <source lang="cpp"> void f() { Investment *pInv = createInvestment(); // вызвать фабричную функцию ... // использовать pInv delete pInv; // освободить память, занятую } // объектом </source> <P>Выглядит хорошо, но есть несколько случаев, когда f не удастся удалить объект инвестиций, полученный от createlnvestment. Где-нибудь внутри непоказанной части функции может встретиться предложение return. Если такой возврат будет выполнен, то управление никогда не достигнет оператора delete. Похожая ситуация может случиться, если вызов createlnvestment и delete поместить в в цикл, и этот цикл будет прерван в результате выполнения goto или continue. И наконец, некоторые предложения внутри части, обозначенной «…», могут возбудить исключение. И в этом случае управление не дойдет до оператора delete. Независимо от того, почему delete будет пропущен, мы потеряем не только память, выделенную для объекта Investment, но и все ресурсы, которые он захватил.</P> <P>Конечно, тщательное программирование может предотвратить ошибки подобного рода, но подумайте о том, как может измениться код со временем. При сопровождении программы кто-то может добавить предложение return или continue, не вполне понимая последствий своих действий для стратегии управления ресурсами, реализованной в данной функции. Хуже того, часть «…» функции f может вызвать функцию, которая никогда не возбуждала исключений, но начнет это делать после некоторого «усовершенствования». То есть полагаться на то, что f всегда доберется до своего оператора delete, просто нельзя.</P> <P>Чтобы обеспечить освобождение ресурса, возвращенного createlnvestment, нам нужно инкапсулировать ресурс внутри объекта, чей деструктор автоматически освободит его, когда управление покинет функцию f. Фактически это половина идеи дела: заключая ресурс в объект, мы можем положиться на автоматический вызов деструкторов C++, чтобы гарантировать их освобождение. (Вторую половину мы обсудим чуть ниже.)</P> <P>Многие ресурсы динамически выделяются из «кучи», используются внутри одного блока или функции и должны быть освобождены, когда управление покидает этот блок или функцию. Для таких ситуаций предназначен класс стандартной библиотеки auto_ptr. Класс auto_ptr описывает объект, подобный указателю (интеллектуальный указатель), чей деструктор автоматически вызывает delete для того, на что он указывает. Вот как использовать auto_ptr для предотвращения потенциальной опасности утечки ресурсов в нашей функции f:</P> <source lang="cpp"> void f() { std::auto_ptr<Investment> pInv(createInvestment()); // вызов фабричной // функции ... // использование pInv как раньше } // автоматическое удаление pInv // деструктором auto_ptr </source> <P>Этот простой пример демонстрирует два наиболее существенных аспекта применения объектов для управления ресурсами:</P> *<STRONG>Ресурс захватывается и сразу преобразуется объект, управлящий им. </STRONG>В приведенном примере ресурс, возвращенный функцией createInvestment, используется для инициализации auto_ptr, который будет им управлять. Фактически идею использования объектов для управления ресурсами часто называют <EM>Получение Ресурса Есть Инициализация</EM> (Resource Acquisition Is Initialization – RAII), поскольку нередко приходится получать ресурс и инициализировать объект управления ресурсом в одном и том же предложении. Иногда полученные ресурсы присваиваются управляющему объекту вместо инициализации, но в любом случае каждый ресурс сразу после получения преобразуется в управляющий им объект. *<STRONG>Управляющие ресурсами объекты используют свои деструкторы для гарантии освобождения ресурсов.</STRONG> Поскольку деструктор вызывается автоматически при уничтожении объекта (например, когда объект выходит из области действия), ресурсы корректно освобождаются независимо от того, как управление покидает блок. Ситуация осложняется, когда в ходе освобождения ресурса может возникнуть исключение, но эта тема обсуждается в [[Правило 8: Не позволяйте исключениям покидать деструкторы | правиле 8]], поэтому сейчас мы о ней говорить не будем. <P>Так как деструктор auto_ptr автоматически удаляет то, на что указывает, важно, чтобы ни в какой момент времени не существовало более одного auto_ptr, указывающего на один и тот же объект. Если такое случается, то объект будет удален более одного раза, что обязательно приведет к неопределенному поведению. Чтобы предотвратить такие проблемы, объекты auto_ptr обладают необычным свойством: при копировании (посредством копирующих конструкторов или операторов присваивания) внутренний указатель в старом объекте становится равным нулю, а новый объект получает ресурс в свое монопольное владение!</P> <source lang="cpp"> std::auto_ptr<Investment> // pInv1 указывает на объект, pInv1(createInvestment()); // возвращенный createInvestment() std::auto_ptr<Investment> pInv2(pInv1); // pInv2 теперь указывает на объект, // а pInv1 равен null pInv1 = pInv2; // теперь pInv1 указывает на объект, // а pInv2 равно null </source> <P>Это странное поведение при копировании плюс лежащее в его основе требование о том, что ни на какой ресурс, управляемый auto_ptr, не должен указывать более чем один auto_ptr, означает, что auto_ptr – не всегда является наилучшим способом управления динамически выделяемыми ресурсами. Например, STL-контейнеры требуют, чтобы их содержимое при копировании вело себя «нормально», поэтому помещать в них объекты auto_ptr нельзя.</P> <P>Альтернатива auto_ptr – это <EM>интеллектуальные указатели с подсчетом ссылок (reference-counting smart pointer – RCSP).</EM> RCSP – это интеллектуальный указатель, который отслеживает, сколько объектов указывают на определенный ресурс, и автоматически удаляет ресурс, когда никто на него не ссылается. Следовательно, RCSP ведет себя подобно сборщику мусора. Но, в отличие от сборщика мусора, RCSP не может разорвать циклические ссылки (когда два неиспользуемых объекта указывают друг на друга).</P> <P>Класс tr1::shared_prt из библиотеки TR1 ([[Правило 54: Ознакомьтесь со стандартной библиотекой, включая TR1 | см. правило 54]]) – это типичный пример RCSP, поэтому вы можете написать:</P> <source lang="cpp"> void f() { ... std::tr1::shared_ptr<Investment> pInv(createStatement()); // вызвать фабричную функцию ... // использовать pInv как раньше } // автоматически удалить pInv // деструктором shared_ptr </source> <P>Этот код выглядит почти так же, как и использующий auto_ptr, но shared_ptr при копировании ведет себя гораздо более естественно:</P> <source lang="cpp"> void f() { ... std::tr1::shared_ptr<Investment> // pInv1 указывает на объект, pInv1(createStatement()); // возвращенный createInvestment std::tr1::shared_ptr<Investment> // теперь оба объекта pInv1 и pInv2 pInv2(pInv1); // указывают на объект pInv1 = pInv2; // ничего не изменилось ... } // pInv1 и pInv2 уничтожены, а объект, // на который они указывали, // автоматически удален </source> <P>Поскольку копирование объектов tr1::shared_ptr работает «как ожидается», то они могут быть использованы в качестве элементов STL-контейнеров, а также в других случаях, когда непривычное поведение auto_ptr нежелательно.</P> <P>Однако не заблуждайтесь. Это правило посвящено не auto_ptr и tr1::shared_ptr, или любым другим типам интеллектуальных указателей. Здесь мы говорим о важности использования объектов для управления ресурсами. auto_ptr и tr1::shared_ptr – всего лишь примеры объектов, которые делают это. (Более подробно о tr1::shared_ptr читайте в правилах [[Правило 14: Тщательно продумывайте поведение при копировании классов, управляющих ресурсами | 14]], [[Правило 18: Проектируйте интерфейсы так, что их легко было использовать правильно и трудно – неправильно | 18]] и [[Правило 54: Ознакомьтесь со стандартной библиотекой, включая TR1 | 54]].)</P> <P>И auto_ptr, и tr1::shared_ptr в своих деструкторах используют оператор delete, а не delete[]. (Разница между ними описана в [[Правило 16: Используйте одинаковые формы new и delete | правиле 16]].) Это значит, что нельзя применять auto_ptr и tr1::shared_ptr к динамически выделенным массивам, хотя, как это ни прискорбно, следующий код скомпилируется:</P> <source lang="cpp"> std::auto_ptr<std::string> // плохая идея! Будет aps(new std::string[10]); // использована не та форма // оператора delete std::tr1::shared_ptr<int> spi(new int[1024]); // та же проблема </source> <P>Вас может удивить, что не предусмотрено ничего подобного auto_ptr или tr1::shared_ptr для работы с динамически выделенными массивами – ни в C++, ни даже в TR1. Это объясняется тем, что такие массивы почти всегда можно заменить векторами или строками (vector и string). Если вы все-таки считаете, что было бы неплохо иметь auto_ptr и tr1::shared_ptr для массивов, обратите внимание на библиотеку Boost ([[Правило 55: Познакомьтесь с Boost | см. правило 55]]). Там вы найдете классы boost::scoped_array и boost::shared_array, которые предоставляют нужное вам поведение.</P> <P>Излагаемые здесь правила по использованию объектов для управления ресурсами предполагают, что если вы освобождаете ресурсы вручную (например, применяя delete помимо того, который содержится в деструкторе управляющего ресурсами класса), то поступаете неправильно. Готовые классы для управления ресурсами – вроде auto_ptr и tr1::shared_ptr – часто облегчают выполнение советов из настоящего правила, но иногда приходится иметь дело с ресурсами, для которых поведение этих классов неадекватно. В таких случаях вам придется разработать собственные классы управления ресурсами. Это не так уж трудно сделать, но нужно принять во внимание некоторые соображения (см. правила [[Правило 14: Тщательно продумывайте поведение при копировании классов, управляющих ресурсами | 14]] и [[Правило 15: Предоставляйте доступ к самим ресурсам из управляющих ими классов | 15]]).</P> <P>И в качестве завершающего комментария я должен сказать, что возврат из функции createInvestment обычного указателя – это путь к утечкам ресурсов, потому что после обращения к ней очень просто забыть вызвать delete для этого указателя. (Даже если используются auto_ptr или tr1::shared_ptr для выполнения delete, нужно не забыть «обернуть» возвращенное значение интеллектуальным указателем.) Чтобы решить эту проблему, нам придется изменить интерфейс createInvestment, и это станет темой [[Правило 18: Проектируйте интерфейсы так, что их легко было использовать правильно и трудно – неправильно | правила 18]].</P> Что следует помнить *Чтобы предотвратить утечку ресурсов, используйте объекты RAII, которые захватывают ресурсы в своих конструкторах и освобождают в деструкторах. *Два часто используемых класса RAII – это tr1::shared_ptr и auto_ptr. Обычно лучше остановить выбор на классе tr1::shared_ptr, потому что его поведение при копировании соответствует интуитивным ожиданиям. Что касается auto_ptr, то после копирования он уже не указывает ни на какой объект. 4de65b14d16633d54fbaa5cb0c64533f1d19c792 38 37 2013-06-17T04:59:21Z Lerom 3360334 wikitext text/x-wiki <P>Предположим, что мы работаем с библиотекой, моделирующей инвестиции (то есть акции, облигации и т. п.), и классы, представляющие разные виды инвестиций, наследуются от корневого класса Investment:</P> <source lang="cpp"> class Investment {...} // корневой класс иерархии // типов инвестиций </source> <P>Предположим далее, что библиотека предоставляет объекты, описывающие конкретные инвестиции, с помощью фабричной функции ([[Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе | см. правило 7]]):</P> </P> <source lang="cpp"> Investment *createInvestment(); // возвращает указатель на динамически // распределенный объект в иерархии // Investment: вызвавший клиент обязан // удалить его (параметры для простоты // опущены) </source> <P>Как следует из комментария, пользователь, вызвавший createlnvestment, отвечает за удаление объекта, возвращенного этой функцией, по окончании его использования. Рассмотрим теперь функцию f, которая это делает:</P> <source lang="cpp"> void f() { Investment *pInv = createInvestment(); // вызвать фабричную функцию ... // использовать pInv delete pInv; // освободить память, занятую } // объектом </source> <P>Выглядит хорошо, но есть несколько случаев, когда f не удастся удалить объект инвестиций, полученный от createlnvestment. Где-нибудь внутри непоказанной части функции может встретиться предложение return. Если такой возврат будет выполнен, то управление никогда не достигнет оператора delete. Похожая ситуация может случиться, если вызов createlnvestment и delete поместить в в цикл, и этот цикл будет прерван в результате выполнения goto или continue. И наконец, некоторые предложения внутри части, обозначенной «…», могут возбудить исключение. И в этом случае управление не дойдет до оператора delete. Независимо от того, почему delete будет пропущен, мы потеряем не только память, выделенную для объекта Investment, но и все ресурсы, которые он захватил.</P> <P>Конечно, тщательное программирование может предотвратить ошибки подобного рода, но подумайте о том, как может измениться код со временем. При сопровождении программы кто-то может добавить предложение return или continue, не вполне понимая последствий своих действий для стратегии управления ресурсами, реализованной в данной функции. Хуже того, часть «…» функции f может вызвать функцию, которая никогда не возбуждала исключений, но начнет это делать после некоторого «усовершенствования». То есть полагаться на то, что f всегда доберется до своего оператора delete, просто нельзя.</P> <P>Чтобы обеспечить освобождение ресурса, возвращенного createlnvestment, нам нужно инкапсулировать ресурс внутри объекта, чей деструктор автоматически освободит его, когда управление покинет функцию f. Фактически это половина идеи дела: заключая ресурс в объект, мы можем положиться на автоматический вызов деструкторов C++, чтобы гарантировать их освобождение. (Вторую половину мы обсудим чуть ниже.)</P> <P>Многие ресурсы динамически выделяются из «кучи», используются внутри одного блока или функции и должны быть освобождены, когда управление покидает этот блок или функцию. Для таких ситуаций предназначен класс стандартной библиотеки auto_ptr. Класс auto_ptr описывает объект, подобный указателю (интеллектуальный указатель), чей деструктор автоматически вызывает delete для того, на что он указывает. Вот как использовать auto_ptr для предотвращения потенциальной опасности утечки ресурсов в нашей функции f:</P> <source lang="cpp"> void f() { std::auto_ptr<Investment> pInv(createInvestment()); // вызов фабричной // функции ... // использование pInv как раньше } // автоматическое удаление pInv // деструктором auto_ptr </source> <P>Этот простой пример демонстрирует два наиболее существенных аспекта применения объектов для управления ресурсами:</P> *<STRONG>Ресурс захватывается и сразу преобразуется объект, управлящий им. </STRONG>В приведенном примере ресурс, возвращенный функцией createInvestment, используется для инициализации auto_ptr, который будет им управлять. Фактически идею использования объектов для управления ресурсами часто называют <EM>Получение Ресурса Есть Инициализация</EM> (Resource Acquisition Is Initialization – RAII), поскольку нередко приходится получать ресурс и инициализировать объект управления ресурсом в одном и том же предложении. Иногда полученные ресурсы присваиваются управляющему объекту вместо инициализации, но в любом случае каждый ресурс сразу после получения преобразуется в управляющий им объект. *<STRONG>Управляющие ресурсами объекты используют свои деструкторы для гарантии освобождения ресурсов.</STRONG> Поскольку деструктор вызывается автоматически при уничтожении объекта (например, когда объект выходит из области действия), ресурсы корректно освобождаются независимо от того, как управление покидает блок. Ситуация осложняется, когда в ходе освобождения ресурса может возникнуть исключение, но эта тема обсуждается в [[Правило 8: Не позволяйте исключениям покидать деструкторы | правиле 8]], поэтому сейчас мы о ней говорить не будем. <P>Так как деструктор auto_ptr автоматически удаляет то, на что указывает, важно, чтобы ни в какой момент времени не существовало более одного auto_ptr, указывающего на один и тот же объект. Если такое случается, то объект будет удален более одного раза, что обязательно приведет к неопределенному поведению. Чтобы предотвратить такие проблемы, объекты auto_ptr обладают необычным свойством: при копировании (посредством копирующих конструкторов или операторов присваивания) внутренний указатель в старом объекте становится равным нулю, а новый объект получает ресурс в свое монопольное владение!</P> <source lang="cpp"> std::auto_ptr<Investment> // pInv1 указывает на объект, pInv1(createInvestment()); // возвращенный createInvestment() std::auto_ptr<Investment> pInv2(pInv1); // pInv2 теперь указывает на объект, // а pInv1 равен null pInv1 = pInv2; // теперь pInv1 указывает на объект, // а pInv2 равно null </source> <P>Это странное поведение при копировании плюс лежащее в его основе требование о том, что ни на какой ресурс, управляемый auto_ptr, не должен указывать более чем один auto_ptr, означает, что auto_ptr – не всегда является наилучшим способом управления динамически выделяемыми ресурсами. Например, STL-контейнеры требуют, чтобы их содержимое при копировании вело себя «нормально», поэтому помещать в них объекты auto_ptr нельзя.</P> <P>Альтернатива auto_ptr – это <EM>интеллектуальные указатели с подсчетом ссылок (reference-counting smart pointer – RCSP).</EM> RCSP – это интеллектуальный указатель, который отслеживает, сколько объектов указывают на определенный ресурс, и автоматически удаляет ресурс, когда никто на него не ссылается. Следовательно, RCSP ведет себя подобно сборщику мусора. Но, в отличие от сборщика мусора, RCSP не может разорвать циклические ссылки (когда два неиспользуемых объекта указывают друг на друга).</P> <P>Класс tr1::shared_prt из библиотеки TR1 ([[Правило 54: Ознакомьтесь со стандартной библиотекой, включая TR1 | см. правило 54]]) – это типичный пример RCSP, поэтому вы можете написать:</P> <source lang="cpp"> void f() { ... std::tr1::shared_ptr<Investment> pInv(createStatement()); // вызвать фабричную функцию ... // использовать pInv как раньше } // автоматически удалить pInv // деструктором shared_ptr </source> <P>Этот код выглядит почти так же, как и использующий auto_ptr, но shared_ptr при копировании ведет себя гораздо более естественно:</P> <source lang="cpp"> void f() { ... std::tr1::shared_ptr<Investment> // pInv1 указывает на объект, pInv1(createStatement()); // возвращенный createInvestment std::tr1::shared_ptr<Investment> // теперь оба объекта pInv1 и pInv2 pInv2(pInv1); // указывают на объект pInv1 = pInv2; // ничего не изменилось ... } // pInv1 и pInv2 уничтожены, а объект, // на который они указывали, // автоматически удален </source> <P>Поскольку копирование объектов tr1::shared_ptr работает «как ожидается», то они могут быть использованы в качестве элементов STL-контейнеров, а также в других случаях, когда непривычное поведение auto_ptr нежелательно.</P> <P>Однако не заблуждайтесь. Это правило посвящено не auto_ptr и tr1::shared_ptr, или любым другим типам интеллектуальных указателей. Здесь мы говорим о важности использования объектов для управления ресурсами. auto_ptr и tr1::shared_ptr – всего лишь примеры объектов, которые делают это. (Более подробно о tr1::shared_ptr читайте в правилах [[Правило 14: Тщательно продумывайте поведение при копировании классов, управляющих ресурсами | 14]], [[Правило 18: Проектируйте интерфейсы так, что их легко было использовать правильно и трудно – неправильно | 18]] и [[Правило 54: Ознакомьтесь со стандартной библиотекой, включая TR1 | 54]].)</P> <P>И auto_ptr, и tr1::shared_ptr в своих деструкторах используют оператор delete, а не delete[]. (Разница между ними описана в [[Правило 16: Используйте одинаковые формы new и delete | правиле 16]].) Это значит, что нельзя применять auto_ptr и tr1::shared_ptr к динамически выделенным массивам, хотя, как это ни прискорбно, следующий код скомпилируется:</P> <source lang="cpp"> std::auto_ptr<std::string> // плохая идея! Будет aps(new std::string[10]); // использована не та форма // оператора delete std::tr1::shared_ptr<int> spi(new int[1024]); // та же проблема </source> <P>Вас может удивить, что не предусмотрено ничего подобного auto_ptr или tr1::shared_ptr для работы с динамически выделенными массивами – ни в C++, ни даже в TR1. Это объясняется тем, что такие массивы почти всегда можно заменить векторами или строками (vector и string). Если вы все-таки считаете, что было бы неплохо иметь auto_ptr и tr1::shared_ptr для массивов, обратите внимание на библиотеку Boost ([[Правило 55: Познакомьтесь с Boost | см. правило 55]]). Там вы найдете классы boost::scoped_array и boost::shared_array, которые предоставляют нужное вам поведение.</P> <P>Излагаемые здесь правила по использованию объектов для управления ресурсами предполагают, что если вы освобождаете ресурсы вручную (например, применяя delete помимо того, который содержится в деструкторе управляющего ресурсами класса), то поступаете неправильно. Готовые классы для управления ресурсами – вроде auto_ptr и tr1::shared_ptr – часто облегчают выполнение советов из настоящего правила, но иногда приходится иметь дело с ресурсами, для которых поведение этих классов неадекватно. В таких случаях вам придется разработать собственные классы управления ресурсами. Это не так уж трудно сделать, но нужно принять во внимание некоторые соображения (см. правила [[Правило 14: Тщательно продумывайте поведение при копировании классов, управляющих ресурсами | 14]] и [[Правило 15: Предоставляйте доступ к самим ресурсам из управляющих ими классов | 15]]).</P> <P>И в качестве завершающего комментария я должен сказать, что возврат из функции createInvestment обычного указателя – это путь к утечкам ресурсов, потому что после обращения к ней очень просто забыть вызвать delete для этого указателя. (Даже если используются auto_ptr или tr1::shared_ptr для выполнения delete, нужно не забыть «обернуть» возвращенное значение интеллектуальным указателем.) Чтобы решить эту проблему, нам придется изменить интерфейс createInvestment, и это станет темой [[Правило 18: Проектируйте интерфейсы так, что их легко было использовать правильно и трудно – неправильно | правила 18]].</P> == Что следует помнить == *Чтобы предотвратить утечку ресурсов, используйте объекты RAII, которые захватывают ресурсы в своих конструкторах и освобождают в деструкторах. *Два часто используемых класса RAII – это tr1::shared_ptr и auto_ptr. Обычно лучше остановить выбор на классе tr1::shared_ptr, потому что его поведение при копировании соответствует интуитивным ожиданиям. Что касается auto_ptr, то после копирования он уже не указывает ни на какой объект. 81f398a1f75b161189b2f74b98cbf9b40258c6e5 Правило 14: Тщательно продумывайте поведение при копировании классов, управляющих ресурсами 0 17 39 2013-06-17T05:23:31Z Lerom 3360334 Новая страница: «<P>В [[Правило 13: Используйте объекты для управления ресурсами | правиле 13]] изложена идея <E…» wikitext text/x-wiki <P>В [[Правило 13: Используйте объекты для управления ресурсами | правиле 13]] изложена идея <EM>Получение Ресурса Есть Инициализация</EM> (Resource Acquisition Is Initialization – RAII), лежащая в основе создания управляющих ресурсами классов. Было также показано, как эта идея воплощается в классах auto_ptr и tr1::shared_ptr для управления динамически выделяемой из кучи памятью. Но не все ресурсы имеют дело с «кучей», и для них интеллектуальные указатели вроде auto_ptr и tr1::shared_ptr обычно не подходят. Время от времени вы будете сталкиваться со случаями, когда понадобится создать собственный класс для управления ресурсами.</P> <P>Например, предположим, что вы используете написанный на языке C интерфейс для работы с мьютексами – объектами типа Mutex, в котором есть функции lock и unlock:</P> <source lang="cpp"> void lock(Mutex *pm); // захватить мьютекс, на который указывает pm void unlock(Mutex *pm); // освободить семафор </source> <P>Чтобы гарантировать, что вы не забудете освободить ранее захваченный Mutex, можно создать управляющий класс. Базовая структура такого класса продиктована принципом RAII, согласно которому ресурс захватывается во время конструирования объекта и освобождается при его уничтожении:</P> <source lang="cpp"> class Lock { public: explicit Lock(Mutex *pm) : mutexPtr(pm) {lock(mutexPtr);} // захват ресурса ~Lock() {unlock(mutexPtr);} // освобождение ресурса private: Mutex *mutexPtr; }; </source> <P>Клиенты используют класс Lock, как того требует идиома RAII:</P> <source lang="cpp"> Mutex m; // определить мьютекс, который вам нужно использовать ... { // создать блок для определения критической секции Lock ml(&m); // захватить мьютекс ... // выполнить операции критической секции } // автоматически освободить мьютекс в конце блока </source> <P>Все прекрасно, но что случится, если скопировать объект Lock?</P> <source lang="cpp"> Lock ml1(&m); // захват m Lock ml2(ml1); // копирование m1 в m2 – что должно произойти? </source> <P>Это частный пример общего вопроса, с которым сталкивается каждый разработчик классов RAII: что должно происходить при копировании RAII-объекта? В большинстве случаев выбирается один из двух вариантов:</P> *<STRONG>Запрет копирования.</STRONG> Во многих случаях не имеет смысла разрешать копирование объектов RAII. Вероятно, это справедливо для класса вроде Lock, потому что редко нужно иметь копии примитивов синхронизации (каковым является мьютекс). Когда копирование RAII-объектов не имеет смысла, вы должны запретить его. [[Правило 6: Явно запрещайте компилятору генерировать функции, которые вам не нужны | Правило 6]] объясняет, как это сделать: объявите копирующие операции закрытыми. Для класса Lock это может выглядеть так: <source lang="cpp"> сlass Lock: private Uncopyable { // запрет копирования – public: // см. правило 6 ... // как раньше }; </source> *<STRONG>Подсчет ссылок на ресурс.</STRONG> Иногда желательно удерживать ресурс до тех пор, пока не будет уничтожен последний объект, который его использует. В этом случае при копировании RAII-объекта нужно увеличивать счетчик числа объектов, ссылающихся на ресурс. Так реализовано «копирование» в классе tr1::shared_ptr. <P>Часто RAII-классы реализуют копирование с подсчетом ссылок путем включения члена типа tr1::shared_ptr&lt;Mutex&gt;. К сожалению, поведение по умолчанию tr1::shared_ptr заключается в том, что он удаляет то, на что указывает, когда значение счетчика ссылок достигает нуля, а это не то, что нам нужно. Когда мы работаем с Mutex, нам нужно просто разблокировать его, а не выполнять delete.</P> <P>К счастью, tr1::shared_ptr позволяет задать «чистильщика» – функцию или функциональный объект, который должен быть вызван, когда счетчик ссылок достигает нуля (эта функциональность не предусмотрена для auto_ptr, который <EM>всегда</EM> удаляет указатель). Функция-чистильщик – это необязательный второй параметр конструктора tr1::shared_ptr, поэтому код должен выглядеть так:</P> <source lang="cpp"> class Lock { public: explicit Lock(Mutex *pm) // инициализировать shared_ptr объектом : mutexPtr(pm, unlock) // Mutex, на который он будет // указывать, функцией unlock { // в качестве чистильщика lock(mutexPtr.get()); } private: std::tr1::shared_ptr<Mutex> mutexPtr; // использовать shared_ptr вместо }; // простого указателя </source> <P>Отметим, что в этом примере в классе Lock больше нет деструктора. Просто в нем отпала необходимость. В [[Правило 5: Какие функции C++ создает и вызывает молча | правиле 5]] объясняется, что деструктор класса (независимо от того, сгенерирован он компилятором или определен пользователем) автоматически вызывает деструкторы нестатических данных-членов класса. В нашем примере это mutexPtr. Но деструктор mutexPtr автоматически вызовет функцию-чистильщик tr1::shared_ptr (в данном случае unlock), когда счетчик ссылок на мьютекс достигнет нуля. (Пользователи, которые будут знакомиться с исходным текстом класса, вероятно, будут благодарны за комментарии, указывающие, что вы не забыли о деструкторе, а просто положились на поведение по умолчанию деструктора, сгенерированного компилятором.)</P> *<STRONG>Копирование управляемого ресурса.</STRONG> Иногда допустимо иметь столько копий ресурса, сколько вам нужно, и единственная причина использования класса, управляющего ресурсами, – гарантировать, что каждая копия ресурса будет освобождена по окончании работы с ней. В этом случае копирование управляющего ресурсом объекта означает также копирование самого ресурса, который в него «обернут». То есть копирование управляющего ресурсом объекта выполняет «глубокое копирование». Некоторые реализации стандартного класса string включают указатели на память из «кучи», где хранятся символы, входящие в строку. Объект такого класса содержит указатель на память из «кучи». Когда объект string копируется, то копируется и указатель, и память, на которую он указывает. Здесь мы снова встречаемся с «глубоким копированием». <STRONG>• Передача владения управляемым ресурсом.</STRONG> Иногда нужно гарантировать, что только один RAII-объект ссылается на ресурс, и при копировании такого объекта RAII владение ресурсом передается объекту-копии. Как объясняется в [[Правило 13: Используйте объекты для управления ресурсами | правиле 13]], это означает копирование с применением auto_ptr. <P>Копирующие функции (конструктор копирования и оператор присваивания) могут быть сгенерированы компилятором, но если сгенерированные версии не делают того, что вам нужно ([[Правило 5: Какие функции C++ создает и вызывает молча | правило 5]] объясняет поведение по умолчанию), придется написать их самостоятельно. Иногда имеет смысл поддерживать обобщенные версии этих функций. Такой подход описан в [[Правило 45: Разрабатывайте шаблоны функций-членов так, чтобы они принимали «все совместимые типы» | правиле 45]].</P> == Что следует помнить == *Копирование RAII-объектов влечет за собой копирование ресурсов, которыми они управляют, поэтому поведение ресурса при копировании определяет поведение RAII-объекта. *Обычно при реализации RAII-классов применяется одна из двух схем: запрет копирования или подсчет ссылок, но возможны и другие варианты. baa6c85fc9c310256fdc5fc5ceadbf88d7ab821a Правило 15: Предоставляйте доступ к самим ресурсам из управляющих ими классов 0 18 40 2013-06-18T08:53:22Z Lerom 3360334 Новая страница: «<P>Управляющие ресурсами классы заслуживают всяческих похвал. Это бастион, защищающий от…» wikitext text/x-wiki <P>Управляющие ресурсами классы заслуживают всяческих похвал. Это бастион, защищающий от утечек ресурсов, а отсутствие таких утечек – фундаментальное свойство хорошо спроектированных систем. В идеальном мире вы можете положиться на эти классы для любых взаимодействий с ресурсами, не утруждая себя доступом к ним напрямую. Но мир неидеален. Многие программные интерфейсы требуют доступа к ресурсам без посредников. Если вы не планируете отказаться от использования таких интерфейсов (что редко имеет смысл на практике), то должны как-то обойти управляющий объект и работать с самим ресурсом.</P> <P>Например, в [[Правило 13: Используйте объекты для управления ресурсами | правиле 13]] изложена идея применения интеллектуальных указателей вроде auto_ptr или tr1::shared_ptr для хранения результата вызова фабричной функции createInvestment:</P> <source lang="cpp"> std::tr1::shared_ptr<Investment> pInv(createInvestment()); // из правила 13 </source> <P>Предположим, есть функция, которую вы хотите применить при работе с объектами класса Investment:</P> <source lang="cpp"> int daysHeld(const Investment *pi); // возвращает количество дней // хранения инвестиций </source> <P>Вы хотите вызывать ее так:</P> <source lang="cpp"> int days = daysHeld(pInv); // ошибка! </source> <BR><P>но этот код не скомпилируется: функция daysHeld ожидает получить указатель на объект класса Investment, а вы передаете ей объект типа tr1::shared_ptr &lt;Investment&gt;.</P> <P>Необходимо как-то преобразовать объект RAII-класса (в данном случае tr1::shared_ptr) к типу управляемого им ресурса (то есть Investment*). Есть два основных способа сделать это: неявное и явное преобразование.</P> <P>И tr1::shared_ptr, и auto_ptr предоставляют функцию-член get для выполнения явного преобразования, то есть возврата (копии) указателя на управляемый объект:</P> <source lang="cpp"> int days = daysHeld(pInv.get()); // нормально, указатель, хранящийся // в pInv, передается daysHeld </source> <P>Как почти все классы интеллектуальных указателей, tr1::shared_ptr и auto_ptr перегружают операторы разыменования указателей (operator-&gt; и operator*), и это обеспечивает возможность неявного преобразования к типу управляемого указателя:</P> <source lang="cpp"> class Investment { // корневой класс иерархии public: // типов инвестиций bool isTaxFree() const; ... }; Investment *createInvestment(); // фабричная функция std::tr1::shared_ptr<Investment> // имеем tr1::shared_ptr pi1(createInvestment()); // для управления ресурсом bool taxable1 = !(pi1->isTaxFree()); // доступ к ресурсу // через оператор -> ... std::auto_ptr<Investment> pi2(createInvestment()); // имеем auto_ptr для // управления ресурсом bool taxable2 = !((*pi2).isTaxFree()); // доступ к ресурсу // через оператор * ... </source> <P>Поскольку иногда необходимо получать доступ к ресурсу, управляемому RAII-объектом, то некоторые реализации RAII предоставляют функции для неявного преобразования. Например, рассмотрим следующий класс для работы со шрифтами, инкапсулирующий «родной» интерфейс, написанный на C:</P> <BR><P><CODE>FontHandle getFont(); // из С API – параметры пропущены</CODE></P> <P><CODE>// для простоты</CODE></P> <P><CODE>void releaseFont(FontHandle fh); // из того же API</CODE></P> <P><CODE>class Font { // класс RAII</CODE></P> <P><CODE>public:</CODE></P> <P><CODE>explicit Font(FontHandle fh) // захватить ресурс:</CODE></P> <P><CODE>:f(fh) // применяется передача по значению,</CODE></P> <P><CODE>{} // потому что того требует C API</CODE></P> <P><CODE>~Font() {releaseFont(f);} // освободить ресурс</CODE></P> <P><CODE>private:</CODE></P> <P><CODE>FontHandle f; // управляемый ресурс – шрифт</CODE></P> <P><CODE>};</CODE></P> <BR><P>Предполагается, что есть обширный программный интерфейс, написанный на C, работающий исключительно в терминах FontHandle. Поэтому часто приходится преобразовывать объекты из типа Font в FontHandle. Класс Font может предоставить функцию явного преобразования, например get:</P> <BR><P><CODE>class Font {</CODE></P> <P><CODE>public:</CODE></P> <P><CODE>...</CODE></P> <P><CODE>FontHandle get() const {return f;} // функция явного преобразования</CODE></P> <P><CODE>...</CODE></P> <P><CODE>};</CODE></P> <BR><P>К сожалению, пользователю придется вызывать get всякий раз при взаимодействии с API:</P> <BR><P><CODE>void changeFontSize(FontHandle f, int newSize); // из C API</CODE></P> <P><CODE>Font f(getFont());</CODE></P> <P><CODE>int newFontSize;</CODE></P> <P><CODE>...</CODE></P> <P><CODE>changeFontSize(f.get(), newFontSize); // явное преобразование</CODE></P> <P><CODE>// из Font в FontHandle</CODE></P> <BR><P>Некоторые программисты могут посчитать, что каждый раз выполнять явное преобразование настолько обременительно, что вообще откажутся от применения этого класса. В результате возрастет опасность утечки шрифтов, а именно для того, чтобы предотвратить это, и был разработан класс Font.</P> <P>Альтернативой может стать предоставление классом Font функции неявного преобразования к FontHandle:</P> <BR><P><CODE>class Font {</CODE></P> <P><CODE>public:</CODE></P> <P><CODE>...</CODE></P> <P><CODE>operator FontHandle() const // функция неявного преобразования</CODE></P> <P><CODE>{return f;}</CODE></P> <P><CODE>...</CODE></P> <P><CODE>};</CODE></P> <BR><P>Это сделает вызовы C API простыми и естественными:</P> <BR><P><CODE>Font f(getFont());</CODE></P> <P><CODE>int newSize;</CODE></P> <P><CODE>...</CODE></P> <P><CODE>changeFontSize(f, newFontSize); // неявное преобразование из Font</CODE></P> <P><CODE>// в FontHandle</CODE></P> <BR><P>Увы, у этого решения есть и оборотная сторона: повышается вероятность ошибок. Например, пользователь может нечаянно создать объект FontHandle, имея в виду Font:</P> <BR><P><CODE>Font f1(getFont());</CODE></P> <P><CODE>...</CODE></P> <P><CODE>FontHandle f2 = f1; // Ошибка! Предполагалось скопировать объект Font,</CODE></P> <P><CODE>// а вместо f1 неявно преобразован в управляемый</CODE></P> <P><CODE>// им FontHandle, который и скопирован в f2</CODE></P> <BR><P>Теперь в программе есть FontHandle, управляемый объектом Font f1, однако он же доступен и напрямую, как f2. Это почти всегда нехорошо. Например, если f1 будет уничтожен, шрифт освобождается, и f2 становится «висячей ссылкой».</P> <P>Решение о том, когда нужно предоставить явное преобразование RAII-объекта к управляемому им ресурсу (посредством функции get), а когда – неявное, зависит от конкретной задачи, для решения которой был спроектирован класс, и условий его применения. Похоже, что лучшее решение – следовать советам правила 18, а именно: делать интерфейсы простыми для правильного применения и трудными – для неправильного. Часто явное преобразование типа функции get – более предпочтительный вариант, поскольку минимизирует шанс получить нежелательное преобразование типов. Однако иногда естественность применения неявного преобразования поможет сделать ваш код чище.</P> <P>Может показаться, что функции, обеспечивающие доступ к управляемым ресурсам, противоречат принципам инкапсуляции. Верно, но в данном случае это не беда. Дело в том, что RAII-классы существуют не для того, чтобы что-то инкапсулировать. Их назначение – гарантировать, что определенное действие (а именно освобождение ресурса) обязательно произойдет. При желании инкапсуляцию ресурса можно реализовать поверх основной функциональности, но это не является необходимым. Более того, некоторые RAII-классы комбинируют истинную инкапсуляцию реализации с отказом от нее в отношении управляемого ресурса. Например, tr1::shared_ptr инкапсулирует подсчет ссылок, но предоставляет простой доступ к управляемому им указателю. Как и большинство хорошо спроектированных классов, он скрывает то, что клиенту не нужно видеть, но обеспечивает доступ к тому, что клиенту необходимо.</P> <H2><STRONG><EM><a name=label45 style="border:none;"></a>Что следует помнить</EM></STRONG></H2><P>• Программные интерфейсы (API) часто требуют прямого обращения к ресурсам. Именно поэтому каждый RAII-класс должен предоставлять возможность получения доступа к ресурсу, которым он управляет.</P> <P>• Доступ может быть обеспечен посредством явного либо неявного преобразования. Вообще говоря, явное преобразование безопаснее, но неявное более удобно для пользователей.</P> 01bc63fc5449fd6bd556b4fb63e6dc34b785144b 41 40 2013-06-18T11:28:25Z Lerom 3360334 wikitext text/x-wiki <P>Управляющие ресурсами классы заслуживают всяческих похвал. Это бастион, защищающий от утечек ресурсов, а отсутствие таких утечек – фундаментальное свойство хорошо спроектированных систем. В идеальном мире вы можете положиться на эти классы для любых взаимодействий с ресурсами, не утруждая себя доступом к ним напрямую. Но мир неидеален. Многие программные интерфейсы требуют доступа к ресурсам без посредников. Если вы не планируете отказаться от использования таких интерфейсов (что редко имеет смысл на практике), то должны как-то обойти управляющий объект и работать с самим ресурсом.</P> <P>Например, в [[Правило 13: Используйте объекты для управления ресурсами | правиле 13]] изложена идея применения интеллектуальных указателей вроде auto_ptr или tr1::shared_ptr для хранения результата вызова фабричной функции createInvestment:</P> <source lang="cpp"> std::tr1::shared_ptr<Investment> pInv(createInvestment()); // из правила 13 </source> <P>Предположим, есть функция, которую вы хотите применить при работе с объектами класса Investment:</P> <source lang="cpp"> int daysHeld(const Investment *pi); // возвращает количество дней // хранения инвестиций </source> <P>Вы хотите вызывать ее так:</P> <source lang="cpp"> int days = daysHeld(pInv); // ошибка! </source> <BR><P>но этот код не скомпилируется: функция daysHeld ожидает получить указатель на объект класса Investment, а вы передаете ей объект типа tr1::shared_ptr &lt;Investment&gt;.</P> <P>Необходимо как-то преобразовать объект RAII-класса (в данном случае tr1::shared_ptr) к типу управляемого им ресурса (то есть Investment*). Есть два основных способа сделать это: неявное и явное преобразование.</P> <P>И tr1::shared_ptr, и auto_ptr предоставляют функцию-член get для выполнения явного преобразования, то есть возврата (копии) указателя на управляемый объект:</P> <source lang="cpp"> int days = daysHeld(pInv.get()); // нормально, указатель, хранящийся // в pInv, передается daysHeld </source> <P>Как почти все классы интеллектуальных указателей, tr1::shared_ptr и auto_ptr перегружают операторы разыменования указателей (operator-&gt; и operator*), и это обеспечивает возможность неявного преобразования к типу управляемого указателя:</P> <source lang="cpp"> class Investment { // корневой класс иерархии public: // типов инвестиций bool isTaxFree() const; ... }; Investment *createInvestment(); // фабричная функция std::tr1::shared_ptr<Investment> // имеем tr1::shared_ptr pi1(createInvestment()); // для управления ресурсом bool taxable1 = !(pi1->isTaxFree()); // доступ к ресурсу // через оператор -> ... std::auto_ptr<Investment> pi2(createInvestment()); // имеем auto_ptr для // управления ресурсом bool taxable2 = !((*pi2).isTaxFree()); // доступ к ресурсу // через оператор * ... </source> <P>Поскольку иногда необходимо получать доступ к ресурсу, управляемому RAII-объектом, то некоторые реализации RAII предоставляют функции для неявного преобразования. Например, рассмотрим следующий класс для работы со шрифтами, инкапсулирующий «родной» интерфейс, написанный на C:</P> <source lang="cpp"> FontHandle getFont(); // из С API – параметры пропущены для простоты void releaseFont(FontHandle fh); // из того же API class Font { // класс RAII public: explicit Font(FontHandle fh) // захватить ресурс: :f(fh) // применяется передача по значению, {} // потому что того требует C API ~Font() {releaseFont(f);} // освободить ресурс private: FontHandle f; // управляемый ресурс – шрифт }; </source> <P>Предполагается, что есть обширный программный интерфейс, написанный на C, работающий исключительно в терминах FontHandle. Поэтому часто приходится преобразовывать объекты из типа Font в FontHandle. Класс Font может предоставить функцию явного преобразования, например get:</P> <source lang="cpp"> class Font { public: ... FontHandle get() const {return f;} // функция явного преобразования ... }; </source> <P>К сожалению, пользователю придется вызывать get всякий раз при взаимодействии с API:</P> <source lang="cpp"> void changeFontSize(FontHandle f, int newSize); // из C API Font f(getFont()); int newFontSize; ... changeFontSize(f.get(), newFontSize); // явное преобразование // из Font в FontHandle </source> <P>Некоторые программисты могут посчитать, что каждый раз выполнять явное преобразование настолько обременительно, что вообще откажутся от применения этого класса. В результате возрастет опасность утечки шрифтов, а именно для того, чтобы предотвратить это, и был разработан класс Font.</P> <P>Альтернативой может стать предоставление классом Font функции неявного преобразования к FontHandle:</P> <source lang="cpp"> class Font { public: ... operator FontHandle() const // функция неявного преобразования {return f;} ... }; </source> <P>Это сделает вызовы C API простыми и естественными:</P> <source lang="cpp"> Font f(getFont()); int newSize; ... changeFontSize(f, newFontSize); // неявное преобразование из Font // в FontHandle </source> <P>Увы, у этого решения есть и оборотная сторона: повышается вероятность ошибок. Например, пользователь может нечаянно создать объект FontHandle, имея в виду Font:</P> <source lang="cpp"> Font f1(getFont()); ... FontHandle f2 = f1; // Ошибка! Предполагалось скопировать объект Font, // а вместо f1 неявно преобразован в управляемый // им FontHandle, который и скопирован в f2 </source> <P>Теперь в программе есть FontHandle, управляемый объектом Font f1, однако он же доступен и напрямую, как f2. Это почти всегда нехорошо. Например, если f1 будет уничтожен, шрифт освобождается, и f2 становится «висячей ссылкой».</P> <P>Решение о том, когда нужно предоставить явное преобразование RAII-объекта к управляемому им ресурсу (посредством функции get), а когда – неявное, зависит от конкретной задачи, для решения которой был спроектирован класс, и условия его применения. Похоже, что лучшее решение – следовать советам [[Правило 18: Проектируйте интерфейсы так, что их легко было использовать правильно и трудно – неправильно | правила 18]], а именно: делать интерфейсы простыми для правильного применения и трудными – для неправильного. Часто явное преобразование типа функции get – более предпочтительный вариант, поскольку минимизирует шанс получить нежелательное преобразование типов. Однако иногда естественность применения неявного преобразования поможет сделать ваш код чище.</P> <P>Может показаться, что функции, обеспечивающие доступ к управляемым ресурсам, противоречат принципам инкапсуляции. Верно, но в данном случае это не беда. Дело в том, что RAII-классы существуют не для того, чтобы что-то инкапсулировать. Их назначение – гарантировать, что определенное действие (а именно освобождение ресурса) обязательно произойдет. При желании инкапсуляцию ресурса можно реализовать поверх основной функциональности, но это не является необходимым. Более того, некоторые RAII-классы комбинируют истинную инкапсуляцию реализации с отказом от нее в отношении управляемого ресурса. Например, tr1::shared_ptr инкапсулирует подсчет ссылок, но предоставляет простой доступ к управляемому им указателю. Как и большинство хорошо спроектированных классов, он скрывает то, что клиенту не нужно видеть, но обеспечивает доступ к тому, что клиенту необходимо.</P> == Что следует помнить == *Программные интерфейсы (API) часто требуют прямого обращения к ресурсам. Именно поэтому каждый RAII-класс должен предоставлять возможность получения доступа к ресурсу, которым он управляет. *Доступ может быть обеспечен посредством явного либо неявного преобразования. Вообще говоря, явное преобразование безопаснее, но неявное более удобно для пользователей. a9b4a56713368080c107b280696c3377cdb81201 Правило 18: Проектируйте интерфейсы так, что их легко было использовать правильно и трудно – неправильно 0 19 42 2013-06-24T07:35:24Z Lerom 3360334 Новая страница: «C++ изобилует интерфейсами. Интерфейсы функций. Интерфейсы классов. Интерфейсы шаблонов. …» wikitext text/x-wiki C++ изобилует интерфейсами. Интерфейсы функций. Интерфейсы классов. Интерфейсы шаблонов. Каждый интерфейс – это средство, посредством которого пользователь взаимодействует с вашим кодом. Предположим, что вы имеете дело с разумными людьми, которые стремятся хорошо сделать свою работу. Они <EM>хотят</EM> применять ваши интерфейсы корректно. Если случится, что они применят какой-то из них неправильно, то часть вины за это ляжет на вас. В идеале, при попытке использовать интерфейс так, что пользователь не получит ожидаемого результата, код не должен компилироваться. А если компилируется, то должен делать то, что имел в виду пользователь.</P> <P>При разработке интерфейсов, простых для правильного применения и трудных – для неправильного, вы должны предвидеть, какие ошибки может допустить пользователь. Например, предположим, что вы разрабатываете конструктор класса, представляющего дату:</P> <source lang="cpp"> class Date { public: Date(int month, int day, int year); ... }; </source> <P>На первый взгляд, этот интерфейс может показаться разумным (во всяком случае, в США), но есть, по крайней мере, две ошибки, которые легко может допустить пользователь. Во-первых, он может передать параметры в неправильном порядке:</P> <source lang="cpp"> Date(30, 3, 1995); // должно быть “3, 30”, а не “30, 3” </source> <P>Во-вторых, номер месяца или дня может быть указан неверно:</P> <source lang="cpp"> Date(2, 20, 1995); // Должно быть “3, 30”, а не “2, 20” </source> <P>(Последний пример может показаться надуманным, но вспомните, что на клавиатуре «2» находится рядом с «3». Такие опечатки случаются сплошь и рядом.)</P> <P>Многих ошибок можно избежать за счет введения новых типов. Система контроля типов – ваш первый союзник в деле предотвращения компилируемости нежелательного кода. В данном случае мы можем ввести простые типы-обертки, чтобы различать дни, месяцы и годы, затем использовать их в конструкторе Date:</P> <source lang="cpp"> struct Day { struct Month { struct Year { explicit Day(int d) explicit Month(int m) explicit Year(int y) : val(d) {} : val(m) {} : val(y) {} int val; int val; int val; }; }; }; class Date { public: Date(const Month& m, const Day& d, const Year& y(; ... }; Date d(30, 3, 1995); // ошибка! неправильные типы Date d(Day(30), Month(3), Year(1995); // ошибка! неправильные типы Date d(Month(3), Day(30), Year(1995)); // порядок, типы корректны </source> <P>Еще лучше сделать Day, Month и Year полноценными классами, инкапсулирующими свои данные ([[Правило 22: Объявляйте данные-члены закрытыми | см. правило 22]]). Но даже применение простых структур наглядно демонстрирует, что разумное использование новых типов способно эффективно предотвратить ошибки при использовании интерфейсов.</P> <P>После того как определены правильные типы, иногда имеет смысл ограничить множество принимаемых ими значений. Например, есть только 12 допустимых значений месяцев, что и должен отразить тип Month. Один из способов сделать это – применить перечисление (enum) для представления месяца. Но перечисления не так безопасны по отношению к типам, как хотелось бы. Например, перечисления могут быть использованы как значения типа int ([[Предпочитайте const, enum и inline использованию #define | см. правило 2]]). Более безопасное решение – определить набор допустимых месяцев:</P> <source lang="cpp"> class Month { public: static Month Jan() {return Month(1);} // функции возвращают все static Month Feb() {return Month(2);} // допустимые значения Month. ... // Cм. ниже, почему это функции, static Month Dec() {return Month(12);} // а не объекты ... // прочие функции-члены private: explicit Month(int m); // предотвращает создание новых // значений Month ... // специфичные для месяца данные }; Date d(Month::Mar(), Day(30), Year(1995)); </source> <P>Идея применения функций вместо объектов для представления месяцев может показаться вам необычной. Но вспомните о ненадежности инициализации нелокальных статических объектов. [[Правило 4: Прежде чем использовать объекты, убедитесь, что они инициализированы | Правило 4]] поможет освежить вашу память.</P> <P>Другой способ предотвратить вероятные ошибки клиентов – ограничить множество разрешенных для типа операций. Общий способ установить ограничения – добавить const. Например, в [[Правило 3: Везде, где только можно используйте const | правиле 3]] объясняется, как добавление модификатора const к типу значения, возвращаемого функцией operator*, может предотвратить следующую ошибку клиента:</P> <source lang="cpp"> if(a *b = c)... // имелось в виду сравнение </source> <P>Фактически это пример другого общего правила облегчения правильного использования типов и усложнения неправильного их использования: поведение ваших типов должно быть согласовано с поведением встроенных типов (кроме некоторых исключительных случаев). Клиенты уже знают, как должны себя вести типы вроде int, поэтому вы должны стараться, чтобы ваши типы по возможности вели себя аналогично. Например, присваивание выражению a*b недопустимо, если a и b – целые, поэтому если нет веской причины отклониться от этого поведения, оно должно быть недопустимо и для ваших типов. Когда сомневаетесь, делайте так, как ведет себя int.</P> <P>Избегать неоправданных расхождений с поведением встроенных типов необходимо для того, чтобы обеспечить согласованность интерфейсов. Из всех характеристик простых для применения интерфейсов согласованность – наверное, самая важная. И наоборот, несогласованность – прямая дорога к ухудшению качества интерфейса. Интерфейсы STL-контейнеров в большинстве случаев согласованы (хотя и не идеально), и это немало способствует простоте их использования. Например, каждый STL-контейнер имеет функцию-член size, которая сообщает, сколько объектов содержится в контейнере. Напротив, в языке Java для массивов используется <EM>свойство</EM> length, для класса String – <EM>метод</EM> length, а для класса List – метод size. Также и в. NET: класс Array имеет свойство Length, а класс ArrayList – свойство Count. Некоторые разработчики считают, что интегрированные среды разработки (IDE) делают эти несоответствия несущественными, но они ошибаются. Несоответствия мешают программисту продуктивно работать, и ни одна IDE это не компенсирует.</P> <P>Любой интерфейс, который требует, чтобы пользователь что-то помнил, может быть использован неправильно, ибо пользователь вполне способен забыть, что от него требуется. Например, в [[Правило 13: Используйте объекты для управления ресурсами | правиле 13]] представлена фабричная функция, которая возвращает указатель на динамически распределенный объект в иерархии Investment:</P> <source lang="cpp"> Investment *createInvestment(); // из правила 13: параметры // для простоты опущены </source> <P>Чтобы избежать утечки ресурсов, указатель, возвращенный createInvestment, обязательно должен быть удален. Следовательно, пользователь может совершить, по крайней мере, две ошибки: забыть удалить указатель либо удалить его более одного раза.</P> <P>[[Правило 13: Используйте объекты для управления ресурсами | Правило 13]] показывает, как клиенты могут поместить значение, возвращенное createInvestment, в «интеллектуальный» указатель наподобие auto_ptr или tr1::shared_ptr, возложив тем самым на него ответственность за вызов delete. Но что, если клиент забудет применить «интеллектуальный» указатель? Во многих случаях для предотвращения этой проблемы лучше было бы написать фабричную функцию, которая сама возвращает «интеллектуальный» указатель:</P> <source lang="cpp"> std::tr1::shared_ptr<Investment> createInvestment(); </source> <P>Тогда пользователь будет вынужден сохранять возвращаемое значение в объекте типа tr1::shared_ptr, и ему не придется помнить о том, что объект Investment по завершении работы с ним необходимо удалить.</P> <P>Фактически возврат значения типа tr1::shared_ptr позволяет проектировщику интерфейса предотвратить и многие другие ошибки, связанные с освобождением ресурса, потому что, как объяснено в [[Правило 14: Тщательно продумывайте поведение при копировании классов, управляющих ресурсами | правиле 14]], tr1::shared_ptr допускает привязку функции-чистильщика к интеллектуальному указателю при его создании (auto_ptr не имеет такой возможности).</P> <P>Предположим, что от пользователя, который получил указатель Investment* от createInvestment, ожидается, что в конце работы он передаст его функции getRidOfInvestment, вместо того чтобы применить к нему delete. Подобный интерфейс – прямая дорога к другой ошибке, заключающейся в использовании не того механизма удаления ресурсов (пользователь может все-таки вызвать delete вместо getRidOfInvestment). Реализация createInvestment может снять эту проблему за счет того, что вернет tr1::shared_ptr с привязанной к нему в качестве чистильщика функцией getRidOfInvestment.</P> <P>Конструктор tr1::shared_ptr принимает два аргумента: указатель, которым нужно управлять, и функцию-чистильщик, которая должна быть вызвана, когда счетчик ссылок достигнет нуля. Это наводит на мысль попытаться следующим образом создать нулевой указатель tr1::shared_ptr с getRidOfInvestment в качестве чистильщика:</P> <BR><P><CODE>std::tr1_shared_ptr&lt;Investment&gt; // попытка создать нулевой shared_ptr</CODE></P> <P><CODE>pInv(0, getRidOfInvestment); // с чистильщиком</CODE></P> <P><CODE>// <EM>это не скомпилируется</EM></CODE></P> <BR><P>К сожалению, C++ это не приемлет. Конструктор tr1::shared_ptr требует, чтобы его первый параметр был указателем, а 0 – это не указатель, это целое. Да, оно <EM>преобразуется</EM> в указатель, но для данного случая этого недостаточно: tr1::shared_ptr настаивает на настоящем указателе. Приведение типа решает эту проблему:</P> <BR><P><CODE>std::tr1_shared_ptr&lt;Investment&gt; // создает null shared_ptr</CODE></P> <P><CODE>pInv(static_cast&lt;Investment*&gt;(0), // с getRidOfInvestment в качестве</CODE></P> <P><CODE>getRidOfInvestment); // чистильщика. о static_cast см.</CODE></P> <P><CODE>// в правиле 27</CODE></P> <BR><P>Это значит, что код, реализующий createInvestment, который должен возвратить tr1::shared_ptr с getRidOfInvestment в качества чистильщика, будет выглядеть примерно так:</P> <BR><P><CODE>std::tr1::shared_ptr&lt;Investment&gt; createInvestment()</CODE></P> <P><CODE>{</CODE></P> <P><CODE>std::tr1::shared_ptr&lt;Investment&gt; retVal(static_cast&lt;Investment*&gt;(0),</CODE></P> <P><CODE>getRidOfInvestment);</CODE></P> <P><CODE>retVal = ...; // retVal должен указывать</CODE></P> <P><CODE>// на корректный объект</CODE></P> <P><CODE>return retVal;</CODE></P> <P><CODE>}</CODE></P> <BR><P>Конечно, если указатель, которым должен управлять pInv, можно было бы определить до создания pInv, то лучше было бы передать его конструктору pInv вместо инициализации pInv нулем с последующим присваиванием значения (см. правило 26).</P> <P>Особенно симпатичное свойство tr1::shared_ptr заключается в том, что он автоматически использует определенного пользователем чистильщика, чтобы избежать другой потенциальной ошибки пользователя – «проблемы нескольких DLL». Она возникает, если объект создается оператором new в одной динамически скомпонованной библиотеке (DLL), а удаляется оператором delete в другой. На многих платформах в такой ситуации возникает ошибка во время исполнения. tr1::shared_ptr решает эту проблемы, поскольку его чистильщик по умолчанию использует delete из той же самой DLL, где был создан tr1::shared_ptr. Это значит, например, что если класс Stock является производным от Investment и функция createInvestment реализована следующим образом:</P> <BR><P><CODE>std::tr1::shared_ptr&lt;Investment&gt; createInvestment()</CODE></P> <P><CODE>{</CODE></P> <P><CODE>return std::tr1::shared_ptr&lt;Investment&gt;(new Stock);</CODE></P> <P><CODE>}</CODE></P> <BR><P>то возвращенный ей объект tr1::shared_ptr можно передавать между разными DLL без риска столкнуться с описанной выше проблемой. Объект tr1::shared_ptr, указывающий на Stock, «помнит», из какой DLL должен быть вызван delete, когда счетчик ссылок на Stock достигнет нуля.</P> <P>Впрочем, этот правило не о tr1::shared_ptr, а о том, как делать интерфейсы легкими для правильного использования и трудными – для неправильного. Но класс tr1::shared_ptr дает настолько простой способ избежать некоторых клиентских ошибок, что на нем стоило остановиться. Наиболее распространенная реализация tr1::shared_ptr находится в библиотеке Boost (см. правило 55). Размер объекта shared_ptr из Boost вдвое больше размера обычного указателя, в нем динамически выделяется память для служебных целей и данных, относящихся к чистильщику, используется вызов виртуальной функции для обращения к чистильщику, производится синхронизация потоков при изменении значения счетчика ссылок в многопоточной среде. (Вы можете отключить поддержку многопоточности, определив символ препроцессора.) Короче говоря, этот интеллектуальный указатель по размеру больше обычного, работает медленнее и использует дополнительную динамически выделяемую память. Но во многих приложениях эти дополнительные затраты времени исполнения будут незаметны, зато уменьшение числа ошибок пользователей заметят все.</P> <H2><STRONG><EM><a name=label52 style="border:none;"></a>Что следует помнить</EM></STRONG></H2><P>• Хорошие интерфейсы легко использовать правильно и трудно использовать неправильно. Вы должны стремиться обеспечить эти характеристики в ваших интерфейсах.</P> <P>• Для обеспечения корректного использования интерфейсы должны быть согласованы и совместимы со встроенными типами.</P> <P>• Для предотвращения ошибок применяют следующие способы: создание новых типов, ограничение допустимых операций над этими типами, ограничение допустимых значений, а также освобождение пользователя от обязанностей по управлению ресурсами.</P> <P>• Класс tr1::shared_ptr поддерживает пользовательские функции-чистильщики. Это снимает «проблему нескольких DLL» и может быть, в частности, использовано для автоматического освобождения мьютекса (см. правило 14).</P> d43b8b06af1a0127e416e65ccb79f047ed9af2da 43 42 2013-06-24T09:43:52Z Lerom 3360334 wikitext text/x-wiki C++ изобилует интерфейсами. Интерфейсы функций. Интерфейсы классов. Интерфейсы шаблонов. Каждый интерфейс – это средство, посредством которого пользователь взаимодействует с вашим кодом. Предположим, что вы имеете дело с разумными людьми, которые стремятся хорошо сделать свою работу. Они <EM>хотят</EM> применять ваши интерфейсы корректно. Если случится, что они применят какой-то из них неправильно, то часть вины за это ляжет на вас. В идеале, при попытке использовать интерфейс так, что пользователь не получит ожидаемого результата, код не должен компилироваться. А если компилируется, то должен делать то, что имел в виду пользователь.</P> <P>При разработке интерфейсов, простых для правильного применения и трудных – для неправильного, вы должны предвидеть, какие ошибки может допустить пользователь. Например, предположим, что вы разрабатываете конструктор класса, представляющего дату:</P> <source lang="cpp"> class Date { public: Date(int month, int day, int year); ... }; </source> <P>На первый взгляд, этот интерфейс может показаться разумным (во всяком случае, в США), но есть, по крайней мере, две ошибки, которые легко может допустить пользователь. Во-первых, он может передать параметры в неправильном порядке:</P> <source lang="cpp"> Date(30, 3, 1995); // должно быть “3, 30”, а не “30, 3” </source> <P>Во-вторых, номер месяца или дня может быть указан неверно:</P> <source lang="cpp"> Date(2, 20, 1995); // Должно быть “3, 30”, а не “2, 20” </source> <P>(Последний пример может показаться надуманным, но вспомните, что на клавиатуре «2» находится рядом с «3». Такие опечатки случаются сплошь и рядом.)</P> <P>Многих ошибок можно избежать за счет введения новых типов. Система контроля типов – ваш первый союзник в деле предотвращения компилируемости нежелательного кода. В данном случае мы можем ввести простые типы-обертки, чтобы различать дни, месяцы и годы, затем использовать их в конструкторе Date:</P> <source lang="cpp"> struct Day { struct Month { struct Year { explicit Day(int d) explicit Month(int m) explicit Year(int y) : val(d) {} : val(m) {} : val(y) {} int val; int val; int val; }; }; }; class Date { public: Date(const Month& m, const Day& d, const Year& y(; ... }; Date d(30, 3, 1995); // ошибка! неправильные типы Date d(Day(30), Month(3), Year(1995); // ошибка! неправильные типы Date d(Month(3), Day(30), Year(1995)); // порядок, типы корректны </source> <P>Еще лучше сделать Day, Month и Year полноценными классами, инкапсулирующими свои данные ([[Правило 22: Объявляйте данные-члены закрытыми | см. правило 22]]). Но даже применение простых структур наглядно демонстрирует, что разумное использование новых типов способно эффективно предотвратить ошибки при использовании интерфейсов.</P> <P>После того как определены правильные типы, иногда имеет смысл ограничить множество принимаемых ими значений. Например, есть только 12 допустимых значений месяцев, что и должен отразить тип Month. Один из способов сделать это – применить перечисление (enum) для представления месяца. Но перечисления не так безопасны по отношению к типам, как хотелось бы. Например, перечисления могут быть использованы как значения типа int ([[Предпочитайте const, enum и inline использованию #define | см. правило 2]]). Более безопасное решение – определить набор допустимых месяцев:</P> <source lang="cpp"> class Month { public: static Month Jan() {return Month(1);} // функции возвращают все static Month Feb() {return Month(2);} // допустимые значения Month. ... // Cм. ниже, почему это функции, static Month Dec() {return Month(12);} // а не объекты ... // прочие функции-члены private: explicit Month(int m); // предотвращает создание новых // значений Month ... // специфичные для месяца данные }; Date d(Month::Mar(), Day(30), Year(1995)); </source> <P>Идея применения функций вместо объектов для представления месяцев может показаться вам необычной. Но вспомните о ненадежности инициализации нелокальных статических объектов. [[Правило 4: Прежде чем использовать объекты, убедитесь, что они инициализированы | Правило 4]] поможет освежить вашу память.</P> <P>Другой способ предотвратить вероятные ошибки клиентов – ограничить множество разрешенных для типа операций. Общий способ установить ограничения – добавить const. Например, в [[Правило 3: Везде, где только можно используйте const | правиле 3]] объясняется, как добавление модификатора const к типу значения, возвращаемого функцией operator*, может предотвратить следующую ошибку клиента:</P> <source lang="cpp"> if(a *b = c)... // имелось в виду сравнение </source> <P>Фактически это пример другого общего правила облегчения правильного использования типов и усложнения неправильного их использования: поведение ваших типов должно быть согласовано с поведением встроенных типов (кроме некоторых исключительных случаев). Клиенты уже знают, как должны себя вести типы вроде int, поэтому вы должны стараться, чтобы ваши типы по возможности вели себя аналогично. Например, присваивание выражению a*b недопустимо, если a и b – целые, поэтому если нет веской причины отклониться от этого поведения, оно должно быть недопустимо и для ваших типов. Когда сомневаетесь, делайте так, как ведет себя int.</P> <P>Избегать неоправданных расхождений с поведением встроенных типов необходимо для того, чтобы обеспечить согласованность интерфейсов. Из всех характеристик простых для применения интерфейсов согласованность – наверное, самая важная. И наоборот, несогласованность – прямая дорога к ухудшению качества интерфейса. Интерфейсы STL-контейнеров в большинстве случаев согласованы (хотя и не идеально), и это немало способствует простоте их использования. Например, каждый STL-контейнер имеет функцию-член size, которая сообщает, сколько объектов содержится в контейнере. Напротив, в языке Java для массивов используется <EM>свойство</EM> length, для класса String – <EM>метод</EM> length, а для класса List – метод size. Также и в. NET: класс Array имеет свойство Length, а класс ArrayList – свойство Count. Некоторые разработчики считают, что интегрированные среды разработки (IDE) делают эти несоответствия несущественными, но они ошибаются. Несоответствия мешают программисту продуктивно работать, и ни одна IDE это не компенсирует.</P> <P>Любой интерфейс, который требует, чтобы пользователь что-то помнил, может быть использован неправильно, ибо пользователь вполне способен забыть, что от него требуется. Например, в [[Правило 13: Используйте объекты для управления ресурсами | правиле 13]] представлена фабричная функция, которая возвращает указатель на динамически распределенный объект в иерархии Investment:</P> <source lang="cpp"> Investment *createInvestment(); // из правила 13: параметры // для простоты опущены </source> <P>Чтобы избежать утечки ресурсов, указатель, возвращенный createInvestment, обязательно должен быть удален. Следовательно, пользователь может совершить, по крайней мере, две ошибки: забыть удалить указатель либо удалить его более одного раза.</P> <P>[[Правило 13: Используйте объекты для управления ресурсами | Правило 13]] показывает, как клиенты могут поместить значение, возвращенное createInvestment, в «интеллектуальный» указатель наподобие auto_ptr или tr1::shared_ptr, возложив тем самым на него ответственность за вызов delete. Но что, если клиент забудет применить «интеллектуальный» указатель? Во многих случаях для предотвращения этой проблемы лучше было бы написать фабричную функцию, которая сама возвращает «интеллектуальный» указатель:</P> <source lang="cpp"> std::tr1::shared_ptr<Investment> createInvestment(); </source> <P>Тогда пользователь будет вынужден сохранять возвращаемое значение в объекте типа tr1::shared_ptr, и ему не придется помнить о том, что объект Investment по завершении работы с ним необходимо удалить.</P> <P>Фактически возврат значения типа tr1::shared_ptr позволяет проектировщику интерфейса предотвратить и многие другие ошибки, связанные с освобождением ресурса, потому что, как объяснено в [[Правило 14: Тщательно продумывайте поведение при копировании классов, управляющих ресурсами | правиле 14]], tr1::shared_ptr допускает привязку функции-чистильщика к интеллектуальному указателю при его создании (auto_ptr не имеет такой возможности).</P> <P>Предположим, что от пользователя, который получил указатель Investment* от createInvestment, ожидается, что в конце работы он передаст его функции getRidOfInvestment, вместо того чтобы применить к нему delete. Подобный интерфейс – прямая дорога к другой ошибке, заключающейся в использовании не того механизма удаления ресурсов (пользователь может все-таки вызвать delete вместо getRidOfInvestment). Реализация createInvestment может снять эту проблему за счет того, что вернет tr1::shared_ptr с привязанной к нему в качестве чистильщика функцией getRidOfInvestment.</P> <P>Конструктор tr1::shared_ptr принимает два аргумента: указатель, которым нужно управлять, и функцию-чистильщик, которая должна быть вызвана, когда счетчик ссылок достигнет нуля. Это наводит на мысль попытаться следующим образом создать нулевой указатель tr1::shared_ptr с getRidOfInvestment в качестве чистильщика:</P> <source lang="cpp"> std::tr1_shared_ptr<Investment> // попытка создать нулевой shared_ptr pInv(0, getRidOfInvestment); // с чистильщиком // это не скомпилируется </source> <P>К сожалению, C++ это не приемлет. Конструктор tr1::shared_ptr требует, чтобы его первый параметр был указателем, а 0 – это не указатель, это целое. Да, оно <EM>преобразуется</EM> в указатель, но для данного случая этого недостаточно: tr1::shared_ptr настаивает на настоящем указателе. Приведение типа решает эту проблему:</P> <source lang="cpp"> std::tr1_shared_ptr<Investment> // создает null shared_ptr pInv(static_cast<Investment*>(0), // с getRidOfInvestment в качестве getRidOfInvestment); // чистильщика. о static_cast см. // в правиле 27 </source> <P>Это значит, что код, реализующий createInvestment, который должен возвратить tr1::shared_ptr с getRidOfInvestment в качества чистильщика, будет выглядеть примерно так:</P> <source lang="cpp"> std::tr1::shared_ptr<Investment> createInvestment() { std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0), getRidOfInvestment); retVal = ...; // retVal должен указывать // на корректный объект return retVal; } </source> <P>Конечно, если указатель, которым должен управлять pInv, можно было бы определить до создания pInv, то лучше было бы передать его конструктору pInv вместо инициализации pInv нулем с последующим присваиванием значения ([[Правило 26: Откладывайте определение переменных насколько возможно | см. правило 26]]).</P> <P>Особенно симпатичное свойство tr1::shared_ptr заключается в том, что он автоматически использует определенного пользователем чистильщика, чтобы избежать другой потенциальной ошибки пользователя – «проблемы нескольких DLL». Она возникает, если объект создается оператором new в одной динамически скомпонованной библиотеке (DLL), а удаляется оператором delete в другой. На многих платформах в такой ситуации возникает ошибка во время исполнения. tr1::shared_ptr решает эту проблемы, поскольку его чистильщик по умолчанию использует delete из той же самой DLL, где был создан tr1::shared_ptr. Это значит, например, что если класс Stock является производным от Investment и функция createInvestment реализована следующим образом:</P> <source lang="cpp"> std::tr1::shared_ptr<Investment> createInvestment() { return std::tr1::shared_ptr<Investment>(new Stock); } </source> <P>то возвращенный ей объект tr1::shared_ptr можно передавать между разными DLL без риска столкнуться с описанной выше проблемой. Объект tr1::shared_ptr, указывающий на Stock, «помнит», из какой DLL должен быть вызван delete, когда счетчик ссылок на Stock достигнет нуля.</P> <P>Впрочем, этот правило не о tr1::shared_ptr, а о том, как делать интерфейсы легкими для правильного использования и трудными – для неправильного. Но класс tr1::shared_ptr дает настолько простой способ избежать некоторых клиентских ошибок, что на нем стоило остановиться. Наиболее распространенная реализация tr1::shared_ptr находится в библиотеке Boost ([[Правило 55: Познакомьтесь с Boost | см. правило 55]]). Размер объекта shared_ptr из Boost вдвое больше размера обычного указателя, в нем динамически выделяется память для служебных целей и данных, относящихся к чистильщику, используется вызов виртуальной функции для обращения к чистильщику, производится синхронизация потоков при изменении значения счетчика ссылок в многопоточной среде. (Вы можете отключить поддержку многопоточности, определив символ препроцессора.) Короче говоря, этот интеллектуальный указатель по размеру больше обычного, работает медленнее и использует дополнительную динамически выделяемую память. Но во многих приложениях эти дополнительные затраты времени исполнения будут незаметны, зато уменьшение числа ошибок пользователей заметят все.</P> == Что следует помнить == *Хорошие интерфейсы легко использовать правильно и трудно использовать неправильно. Вы должны стремиться обеспечить эти характеристики в ваших интерфейсах. *Для обеспечения корректного использования интерфейсы должны быть согласованы и совместимы со встроенными типами. *Для предотвращения ошибок применяют следующие способы: создание новых типов, ограничение допустимых операций над этими типами, ограничение допустимых значений, а также освобождение пользователя от обязанностей по управлению ресурсами. *Класс tr1::shared_ptr поддерживает пользовательские функции-чистильщики. Это снимает «проблему нескольких DLL» и может быть, в частности, использовано для автоматического освобождения мьютекса ([[Правило 14: Тщательно продумывайте поведение при копировании классов, управляющих ресурсами | см. правило 14]]). 1720735571210bb239d578838a69f26bf0d76170 Правило 19: Рассматривайте проектирование класса как проектирование типа 0 20 44 2013-06-24T10:01:15Z Lerom 3360334 Новая страница: «<P>В C++, как и в других объектно-ориентированных языках программирования, при определении …» wikitext text/x-wiki <P>В C++, как и в других объектно-ориентированных языках программирования, при определении нового класса определяется новый тип. Потому большую часть времени вы как разработчик C++ будете тратить на совершенствование вашей системы типов. Это значит, что вы – не просто разработчик классов, но еще и разработчик типов. Перегруженные функции и операторы, управление распределением и освобождением памяти, определение инициализации и порядка уничтожения объектов – все это находится в ваших руках. Поэтому вы должны подходить к проектированию классов так, как разработчики языка подходят к проектированию встроенных типов.</P> <P>Проектирование хороших классов – ответственная работа, и этим все сказано. Хорошие типы имеют естественный синтаксис, интуитивно воспринимаемую семантику и одну или более эффективных реализаций. В C++ плохо спланированное определение класса может сделать невозможным достижение любой из этих целей. Даже характеристики производительности функций-членов класса могут зависеть от того, как они объявлены.</P> <P>Итак, как же проектировать эффективные классы? Прежде всего вы должны понимать, с чем имеете дело. Проектирование почти любого класса ставит перед разработчиком вопросы, ответы на которые часто ограничивают спектр возможных решений:</P> *<STRONG>Как должны создаваться и уничтожаться объекты нового типа?</STRONG> От ответа на этот вопрос зависит дизайн конструкторов и деструкторов, а равно функций распределения и освобождения памяти (оператор new, оператор new[], оператор delete и оператор delete[] – см. главу 8), если вы собираетесь их переопределить. *<STRONG>Чем должна отличаться инициализация объекта от присваивания значений?</STRONG> Ответ на этот вопрос определяет разницу в поведении между конструкторами и операторами присваивания. Важно не путать инициализацию с присваиванием, потому что им соответствуют разные вызовы функций ([[Правило 4: Прежде чем использовать объекты, убедитесь, что они инициализированы | см. правило 4]]). *<STRONG>Что означает для объектов нового типа быть переданными по значению?</STRONG> Помните, что конструктор копирования определяет реализацию передачи по значению для данного типа. *<STRONG>Каковы ограничения на допустимые значения вашего нового типа?</STRONG> Обычно только некоторые комбинации значений данных-членов класса являются правильными. Эти комбинации определяют инварианты, которые должен поддерживать класс. А инварианты уже диктуют, как следует контролировать ошибки в функциях-членах, в особенности в конструкторах, операторах присваивания и функциях установки значений («setter» functions). Могут быть также затронуты исключения, которые возбуждают ваши функции, и спецификации этих исключений. *<STRONG>Укладывается ли ваш новый тип в граф наследования?</STRONG> Наследуя свои классы от других, вы должны следовать ограничениям, налагаемым базовыми классами. В частности, нужно учитывать, как объявлены в них функции-члены: виртуальными или нет (см. правила [[Правило 34: Различайте наследование интерфейса и наследование реализации | 34]] и [[Правило 36: Никогда не переопределяйте наследуемые невиртуальные функции | 36]]). Если вы хотите, чтобы вашему классу могли наследовать другие, то нужно тщательно продумать, какие функции объявить виртуальными; в особенности это относится к деструктору ([[Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе | см. правило 7]]). *<STRONG>Какие варианты преобразования типов допустимы для вашего нового типа?</STRONG> Ваш тип существует в море других типов, поэтому должны ли быть предусмотрены варианты преобразования между вашим типом и другими? Если вы хотите разрешить <EM>неявное</EM> преобразование объекта типа T1 в объект типа T2, придется либо написать функцию преобразования в классе T1 (то есть operator T2), либо неявный конструктор в классе T2, который может быть вызван с единственным аргументом. Если же вы хотите разрешить только <EM>явные</EM> преобразования, то нужно будет написать специальные функции, но ни в коем случае не делать их операторами преобразования или не-explicit конструкторами с одним аргументом. (Примеры явных и неявных функций преобразования приведены в [[Правило 15: Предоставляйте доступ к самим ресурсам из управляющих ими классов | правиле 15]].) *<STRONG>Какие операторы и функции имеют смысл для нового типа?</STRONG> Ответ на этот вопрос определяет набор функций, которые вы объявляете в вашем классе. Некоторые из них будут функциями-членами, другие – нет (см. правила [[Правило 23: Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса | 23]], [[Правило 24: Объявляйте функции, не являющиеся членами, когда преобразование типов должно быть применимо ко всем параметрам | 24]] и [[Правило 46: Определяйте внутри шаблонов функции, не являющиеся членами, когда желательны преобразования типа | 46]]). *<STRONG> Какие стандартные функции должны стать недоступными?</STRONG> Их надо будет объявить закрытыми ([[Правило 6: Явно запрещайте компилятору генерировать функции, которые вам не нужны | см. правило 6]]). *<STRONG>Кто должен получить доступ к членам вашего нового типа?</STRONG> Ответ на этот вопрос помогает определить, какие члены должны быть открытыми (public), какие – защищенными (protected) и какие – закрытыми (private). Также вам предстоит решить, какие классы и/или функции должны быть друзьями класса, а также когда имеет смысл вложить один класс внутрь другого. *<STRONG>Что такое «необъявленный интерфейс» вашего нового типа?</STRONG> Какого рода гарантии могут быть предоставлены относительно производительности, безопасности относительно исключений ([[Правило 29: Стремитесь, чтобы программа была безопасна относительно исключений | см. правило 29]]) и использования ресурсов (например, блокировок и динамической памяти)? Такого рода гарантии определяют ограничения на реализацию вашего класса. *<STRONG>Насколько общий ваш новый тип?</STRONG> Возможно, в действительности вы не определяете новый тип. Возможно, вы определяете целое <EM>семейство</EM> типов. Если так, то вам нужно определять не новый класс, а новый шаблон класса. *<STRONG>Действительно ли новый тип представляет собой то, что вам нужно?</STRONG> Если вы определяете новый производный класс только для того, чтобы расширить функциональность существующего класса, то, возможно, этой цели лучше достичь простым определением одной или более функций-нечленов либо шаблонов.</P> <P>На эти вопросы нелегко ответить, поэтому определение эффективных классов – непростая задача. Но при ее должном выполнении определенные пользователями классы C++ дают типы, которые ничем не уступают встроенным и уже оправдывают все ваши усилия.</P> == Что следует помнить == *Проектирование класса – это проектирование типа. Прежде чем определять новый тип, убедитесь, что рассмотрены все вопросы, которые обсуждаются в настоящем правиле. 7ecec303b89cf13ef0fe6b1ce44e247e7fd7af9e Правило 23: Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса 0 21 45 2013-06-26T09:29:58Z Lerom 3360334 Новая страница: «<P>Возьмем класс для представления Web-браузера. В числе прочих такой класс может предлага…» wikitext text/x-wiki <P>Возьмем класс для представления Web-браузера. В числе прочих такой класс может предлагать функции, который очищают кэш загруженных элементов, очищают историю посещенных URL и удаляют из системы все «куки» (cookies):</P> <source lang="cpp"> class WebBrowser { public: ... void clearCache(); void clearHistory(); void removeCookies(); ... }; </source> <P>Найдутся пользователи, которые захотят выполнить все эти действия вместе, поэтому WebBrowser может также предоставить функцию и для этой цели:</P> <source lang="cpp"> class WebBrowser { public: ... void clearEveryThing(); // вызывает clearCache(), clearHistory() // и removeCookies() ... }; </source> <P>Конечно, такая функциональность может быть обеспечена также функцией, не являющейся членом класса, которая вызовет соответствующие функции-члены:</P> <source lang="cpp"> void clearBrowser(WebBrowser& wb) { wb.clearCache(); wb.clearHistory(); wb.removeCache(); } </source> <P>Что лучше – функция-член clearEverything или свободная функция clear-Browser?</P> <P>Принципы объектно-ориентированного проектирования диктуют, что данные и функции, которые оперируют ими, должны быть связаны вместе, и это предполагает, что функция-член – лучший выбор. К сожалению, это предположение неверно. Оно основано на непонимании того, что такое «объектно-ориентированный». Да, в объектно-ориентированных программах данные должны быть <EM>инкапсулированы,</EM> насколько возможно. В противоположность интуитивному восприятию функция-член clearEverything в действительности менее инкапсулирована, чем свободная функция clearBrowser. Более того, предоставление свободной функции позволяет обеспечить большую гибкость при «упаковке» функциональности класса WebBrowser, а это приводит к меньшему числу зависимостей на этапе компиляции и расширяет возможности для расширения класса. Поэтому свободная функция лучше по многим причинам. Важно их отчетливо понимать.</P> <P>Начнем с инкапсуляции. Если некая сущность инкапсулируется, она скрывается из виду. Чем больше эта сущность инкапсулирована, тем меньше частей программы могут ее видеть. Чем меньше частей программы могут видеть некую сущность, тем больше гибкости мы имеем для внесения изменений, поскольку изменения напрямую касаются лишь тех частей, которым эти изменения видны. Таким образом, чем больше степень инкапсуляции сущности, тем шире наши возможности вносить в нее изменения. Вот причина того, почему мы ставим инкапсуляцию на первое место: она обеспечивает нам гибкость в изменении кода таким образом, что это затрагивает минимальное количество пользователей.</P> <P>Рассмотрим данные, ассоциированные с объектом. Чем меньше существует кода, который видит эти данные (то есть имеет к ним доступ), тем в большей степени они инкапсулированы и тем свободнее мы можем менять их характеристики, например количество членов-данных, их типы и т. п. Грубой оценкой объем кода, который может видеть некоторый член данных, можно считать число функций, имеющих к нему доступ: чем больше таких функций, тем менее инкапсулированы данные.</P> <P>В [[Правило 22: Объявляйте данные-члены закрытыми | правиле 22]] объясняется, что данные-члены должны быть закрытыми, потому что в противном случае к ним имеет доступ неограниченное число функций. Они вообще не инкапсулированы. Для <EM>закрытых</EM> же данных-членов количество функций, имеющих доступ к ним, определяется количеством функций-членов класса плюс количество функций-друзей, потому что доступ к закрытым членам разрешен только функциям-членам и друзьям класса. Если есть выбор между функцией-членом (которая имеет доступ не только к закрытым данным класса, но также к его закрытым функциям, перечислениям, определениям типов (typedef) и т. п.) и свободной функцией, не являющейся к тому же другом класса (такие функции не имеют доступа ни к чему из вышеперечисленного), но обеспечивающей ту же функциональность, то напрашивается очевидный вывод: большую инкапсуляцию обеспечивает функция, не являющаяся ни членом, ни другом, потому что она не увеличивает числа функций, которые могут иметь доступ к закрытой секции класса. Это объясняет, почему clearBrowser (свободная функция) предпочтительнее, чем clearEverything (функция-член).</P> <P>Здесь стоит обратить внимание на два момента. Первое – все вышесказанное относится только к свободным функциям, <EM>не являющимся друзьями</EM> класса. Друзья имеют такой же доступ к закрытым членам класса, что и функции-члены, а потому точно так же влияют на инкапсуляцию. С точки зрения инкапсуляции, выбор следует делать не между функциями-членами и свободными функциями, а между функциями-членами, с одной стороны, и свободными функциями, не являющимися друзьями, – с другой. (Но оценивать проектное решение надо, конечно, не только с точки зрения инкапсуляции. В [[Правило 24: Объявляйте функции, не являющиеся членами, когда преобразование типов должно быть применимо ко всем параметрам | правиле 24]] объясняется, что когда дело касается неявного приведения типов, то выбирать надо между функциями-членами и свободными функциями.)</P> <P>Во-вторых, из того, что забота об инкапсуляции требует, чтобы функция не была членом класса, вовсе не следует, что эта функция не может быть членом какого-то другого класса. Это может облегчить жизнь программистам, привыкшим к языкам, в которых все функции <EM>должны</EM> быть членами классов (например, Eiffel, Java, C# и т. п.). Например, мы можем сделать clearBrowser статической функцией-членом некоторого служебного класса. До тех пор пока она не является частью (или другом) класса WebBrowser, она никак не скажется на инкапсуляции его закрытых членов.</P> <P>В C++ более естественно объявить clearBrowser свободной функцией в том же пространстве имен, что и класс WebBrowser:</P> <source lang="cpp"> namespace WebBrowserStuff { class WebBrowser {...}; void clearBrowser(WebBrowser& wb); ... } </source> <P>Но дело тут не только в естественности, ведь пространства имен, в отличие от классов, могут быть находиться в нескольких исходных файлах. И это важно, потому что функции вроде clearBrowser являются <EM>вспомогательными.</EM> Не будучи ни членами, ни друзьями класса, они не имеют специального доступа к WebBrowser и никак не могут расширить те возможности, которые у пользователей класса WebBrowser и так уже были. Не будь функции clearBrowser, пользователь мог бы самостоятельно вызвать clearCache, clearHistory и removeCookies.</P> <P>Для класса, подобного WebBrowser, можно было бы определить много таких вспомогательных функций: для работы с закладками, вывода на печать, управления «куками» и т. п. Вообще говоря, большинству пользователей будут интересны только некоторые из этих функций. Но с какой стати компиляция пользовательской программы, в которой используются только функции, относящиеся к закладкам, должна зависеть, например, от наличия функций управления «куками»? Самый простой способ разделить их – это объявить функции, относящиеся к закладкам, в одном заголовочном файле, функции управления «куками» – в другом, функции поддержки печати – в третьем и так далее:</P> <source lang="cpp"> // заголовок “webbrowser.h” – заголовок для самого класса WebBrowser, // а также базовой функциональности, имеющей к нему отношение namespace WebBrowserStuff { class WebBrowser{...}; ... // базовая функциональность, то есть // функции-нечлены, нужные почти всем // клиентам } // заголовок “webbrowserbookmarks.h” namespace WebBrowserStuff { ... // вспомогательные функции, касающиеся } // закладок // заголовок “webbrowsercookies.h” namespace WebBrowserStuff { ... // вспомогательные функции, касающиеся } // “куков” ... </source> <P>Отметим, что именно так организована стандартная библиотека C++. Вместо единственного монолитного заголовка &lt;С++ StandardLibrary&gt;, содержащего все, что есть в пространстве имен std, существуют десятки более мелких заголовочных файлов (например, &lt;vector&gt;, &lt;algorithm&gt;, &lt;memory&gt; и т. п.). В каждом из них объявлена некоторая функциональность из std. Пользователь, которому нужно только то, что имеет отношение к векторам, может не включать в свою программу директиву #include &lt;memory&gt;, а пользователь, не нуждающийся в списках, не обязан включать #include &lt;list&gt;. Поэтому на этапе компиляции пользовательские программы зависят только от тех частей системы, которые они действительно используют (см. в [[Правило 31: Уменьшайте зависимости файлов при компиляции | правиле 31]] обсуждение других способов уменьшения зависимостей компиляции). Подобное разделение функциональности невозможно, если она обеспечивается функциями-членами класса, потому что класс должен быть определен полностью, его нельзя разбить на части.</P> <P>Размещение вспомогательных функций в разных заголовочных файлах, но в одном пространстве имен – означает также, что пользователи могут легко расширять набор вспомогательных функций. Для этого нужно лишь поместить новые функции (нечлены и недрузья) в то же пространство имен. Например, если пользователь класса WebBrowser решит дописать вспомогательные функции, имеющие отношение к загрузке изображений, он должен будет создать заголовочный файл, включающий объявления этих функций в пространство имен Web-BrowserStuff. Новые функции становятся после этого так же доступны, как и все прочие вспомогательные функции. Это еще одно свойство, которое не могут представить классы, потому что определения классов закрыты для расширения клиентами. Конечно, клиенты могут создавать производные классы, но они не будут иметь доступа к инкапсулированным (то есть закрытым) членам базового класса, поэтому таким образом «расширенную» функциональность уже не назовешь первоклассной. Кроме того, в [[Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе | правиле 7]] объясняется, что не все классы предназначены для того, чтобы быть базовыми.</P> == Что следует помнить == *Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса. Это повышает степень инкапсуляции и расширяемости, а также гибкость «упаковки» функциональности. f9294c3da5f5a953aaf63c54367422a2b4968283 Правило 20: Предпочитайте передачу по ссылке на const передаче по значению 0 22 46 2013-06-26T11:35:34Z Lerom 3360334 Новая страница: «<P>По умолчанию в C++ объекты передаются в функции и возвращаются функциями по значению (св…» wikitext text/x-wiki <P>По умолчанию в C++ объекты передаются в функции и возвращаются функциями по значению (свойство, унаследованное от C). Если не указано противное, параметры функции инициализируются копиями реальных аргументов, а после вызова функции программа получает <EM>копию</EM> возвращаемой функцией величины. Копии вырабатываются конструкторами копирования. Поэтому передача по значению может оказаться накладной операцией. Например, рассмотрим следующую иерархию классов:</P> <source lang="cpp"> class Person { public: Person(); // параметры опущены для простоты virtual ~Person(); // см. в правиле 7 – почему виртуальный ... private: std::string name; std::string address; }; class Student: public Person { public: Student(); // и здесь параметры опущены ~ Student(); ... private: std::string schoolName; std::string schoolAddress; }; </source> <P>Теперь взгляните на следующий код, где вызывается функция validateStudent, которая принимает аргумент Student (по значению) и возвращает признак его корректности:</P> <source lang="cpp"> bool validateStudent(Student s); // функция принимает параметр // Student по значению Student plato; // Платон учился у Сократа bool platoIsOk = validateStudent(plato); // вызов функции </source> <P>Что происходит при вызове этой функции?</P> <P>Ясно, что вызывается конструктор копирования Student для инициализации параметра plato. Также ясно, что s уничтожается при возврате из validate-Student. Поэтому передача параметра по значению этой функции обходится в один вызов конструктора копирования Student и один вызов деструктора Student.</P> <P>Но это еще не все. Объект Student содержит внутри себя два объекта string, поэтому каждый раз, когда вы конструируете объект Student, вы должны также конструировать и эти два объекта. Класс Student наследует класу Person, поэтому каждый раз, конструируя объект Student, вы должны сконструировать и объект Person. Но объект Person содержит еще два объекта string, поэтому каждое конструирование Person влечет за собой два вызова конструктора string. Итак, передача объекта Student по значению приводит к одному вызову конструктора копирования Student, одному вызову конструктора копирования Person и четырем вызовам конструкторов копирования string. Когда копия объекта Student разрушается, каждому вызову конструктора соответствует вызов деструктора, поэтому общая стоимость передачи Student по значению составляет шесть конструкторов и шесть деструкторов!</P> <P>Что ж, это корректное и желательное поведение. В конец концов, вы <EM>хотите,</EM> чтобы все ваши объекты были надежно инициализированы и уничтожены. И все же было бы неплохо найти способ пропустить все эти вызовы конструкторов и деструкторов. Способ есть! Это – передача по ссылке на константу:</P> <source lang="cpp"> bool validateStudent(const Student& s); </source> <P>Этот способ гораздо эффективнее: не вызываются никакие конструкторы и деструкторы, поскольку не создаются никакие новые объекты. Квалификатор const в измененном объявлении параметра важен. Исходная версия validateStudent принимала параметр Student по значению, вызвавший ее знает о том, что он защищен от любых изменений, которые функция может внести в переданный ей объект; validateStudent сможет модифицировать только его копию. Теперь же, когда Student передается по ссылке, необходимо объявить его const, поскольку в противном случае вызывающая программа должна побеспокоиться о том, чтобы validateStudent не вносила изменений в переданный ей объект.</P> <P>Передача параметров по ссылке также позволяет избежать проблемы « <EM>срезки</EM> » (slicing). Когда объект производного класса передается (по значению) как объект базового класса, вызывается конструктор копирования базового класса, а те части, которые принадлежат производному, «срезаются». У вас остается только простой объект базового класса – что вполне естественно, так как его создал конструктор базового класса. Это почти всегда не то, что вам нужно. Например, предположим, что вы работаете с набором классов для реализации графической оконной системы:</P> <source lang="cpp"> class Window { public ... std::string name() const; // возвращает имя окна virtual void display() const; // рисует окно и его содержимое }; class WindwoWithScrollBars: public Window { public: ... virtual void display() const; }; </source> <P>Все объекты класса Window имеют имя, которое вы можете получить посредством функции name, и все окна могут быть отображены, на что указывает наличие функции display. Тот факт, что display – функция виртуальная, говорит о том, что способ отображения простых объектов базового класса Window может отличаться от способа отображения объектов WindowWithScrollBar (см. правила [[Правило 34: Различайте наследование интерфейса и наследование реализации | 34]] и [[Правило 36: Никогда не переопределяйте наследуемые невиртуальные функции | 36]]).</P> <P>Теперь предположим, что вы хотите написать функцию, которая будет печатать имя окна и затем отображать его. Вот <EM>неверный</EM> способ написания такой функции:</P> <source lang="cpp"> void printNameAndDisplay(Window w) // неправильно! Параметр { // может быть «срезан» std::cout << w.name(); w.display(); } </source> Посмотрим, что получиться, если вызвать эту функцию, передав ей обект WindowWithScrollBar: <source lang="cpp"> WindowWithScrollBar wwsb; PrintNameAndDisplay(wwsb); </source> <P>Параметр w будет сконструирован – он передан по значению, помните? – как объект Window, и вся дополнительная информация, которая делает его объектом WindowWithScrollBar, будет срезана. Внутри printNameAndDisplay w всегда будет вести себя как объект класса Window (потому что это и есть объект класса Window), независимо от типа объекта, в действительности переданного функции. В частности, вызов функции display внутри printNameAndDisplay всегда вызовет Window::display и никогда – WindowWithScrollBar::display.</P> <P>Способ решения проблемы «срезки» – передать w по ссылке на константу:</P> <source lang="cpp"> void printNameAndDisplay(const Window& w) // правильно, параметр { // не может быть «срезан» std::cout << w.name(); w.display(); } </source> <P>Теперь w ведет себя правильно, какое бы окно он ни представлял в действительности.</P> <P>Если вы заглянете «под капот» C++, то увидите, что ссылки обычно реализуются как указатели, поэтому передача чего-либо по ссылке обычно означает передачу указателя. В результате объекты встроенного типа (например, int) всегда более эффективно передавать по значению, чем по ссылке. Поэтому для встроенных типов, если у вас есть выбор – передавать по значению или по ссылке на константу, имеет смысл выбрать передачу по значению. Тот же совет касается итераторов и функциональных объектов STL, потому что они специально спроектированы для передачи по значению. Программисты, реализующие итераторы и функциональные объекты, отвечают за то, чтобы обеспечить эффективность передачи их по значению и исключить «срезку». Это пример того, как меняются правила в зависимости от используемой вами части C++ ([[Правило 1: Относитесь к C++ как к конгломерату языков | см. правило 1]]).</P> <P>Встроенные типы являются небольшими объектами, поэтому некоторые делают вывод, что все встроенные типы – хорошие кандидаты на передачу по значению, даже если они определены пользователем. Сомнительно. То, что объект небольшой, еще не значит, что вызов его конструктора копирования обойдется дешево. Многие объекты – среди них большинство контейнеров STL – содержат в себе немногим больше обычного указателя, но копирование таких объектов влечет за собой копирование всего, на что они указывают. Это может оказаться <EM>очень</EM> дорого.</P> <P>Даже когда маленькие объекты имеют ненакладные конструкторы копирования, все равно они могут оказывать влияние на производительность. Некоторые компиляторы рассматривают встроенные и пользовательские типы по-разному, даже если они имеют одинаковое внутреннее представление. Например, некоторые компиляторы не размещают объекты, состоящие из одного лишь double в регистрах, даже если готовы размещать там значения встроенного типа double. В таких случаях лучше передавать объекты по ссылке, потому что компилятор безусловно готов поместить в регистр указатель (реализующий ссылку).</P> <P>Другая причина того, почему маленькие пользовательские типы не обязательно хороши для передачи по значению, заключается в том, что их размер подвержен изменениям. Тип, который мал сегодня, может вырасти в будущем, потому что его внутренняя реализация может измениться. Ситуация меняется даже в том случае, если вы переключаетесь на другую реализацию C++. Например, в одних реализациях тип string из стандартной библиотеки <EM>в семь раз больше,</EM> чем в других.</P> <P>Вообще говоря, единственные типы, для которых можно предположить, что передача по значению будет недорогой, – это встроенные типы, а также итераторы и функциональные объекты STL. Для всего остального следуйте совету этого правила и передавайте параметры по ссылке на константу вместо передачи по значению.</P> == Что следует помнить == *Передаче по значению предпочитайте передачу по ссылке на константу. Обычно это более эффективно и позволяет избежать проблемы «срезки». *Это правило не касается встроенных типов, итераторов и функциональных объектов STL. Для них передача по значению обычно подходит больше. 764da332c6475cd4673d886118c9c00ab488056b Правило 24: Объявляйте функции, не являющиеся членами, когда преобразование типов должно быть применимо ко всем параметрам 0 23 47 2013-06-26T12:31:36Z Lerom 3360334 Новая страница: «<P>Во введении я отмечал, что в общем случае поддержка классом неявных преобразований тип…» wikitext text/x-wiki <P>Во введении я отмечал, что в общем случае поддержка классом неявных преобразований типов – неудачная мысль. Но, конечно, из этого правила есть исключения, и одно из наиболее важных касается создания числовых типов. Например, если вы проектируете класс для представления рациональных чисел, то неявное преобразование целого числа в рациональное выглядит вполне разумно. Уж во всяком случае не менее разумно, чем встроенное в C++ преобразование int в double (и куда разумнее встроенного преобразования из double в int). Коли так, то начать объявления класса Rational можно было бы следующим образом:</P> <source lang="cpp"> class Rational { public: Rational(int numerator = 0, int denominator = 1); // конструктор сознательно не explicit; // допускает неявное преобразование // int в Rational int numerator() const; // функции доступа к числителю и int denominator() const; // знаменателю – см. правило 22 private: ... }; </source> <P>Вы знаете, что понадобится поддерживать арифметические операции (сложение, умножение и т. п.), но не уверены, следует реализовывать их посредством функций-членов или свободных функций, возможно, являющихся друзьями класса. Инстинкт говорит: «Сомневаешься – придерживайся объектно-ориентированного подхода». Вы понимаете, что, скажем, умножение рациональных чисел относится к классу Rational, поэтому кажется естественным реализовать operator* в самом этом классе. Но наперекор интуиции [[Правило 23: Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса | правило 23]] утверждает, что идея помещения функции внутрь класса, с которым она ассоциирована, иногда противоречит объектно-ориентированным принципам. Впрочем, оставим на время эту тему и посмотрим, во что выливается объявление operator* функцией-членом Rational:</P> <source lang="cpp"> class Rational { public: ... const Rational operator*(const Rational& rhs) const; } </source> <P>Если вы не понимаете, почему эта функция объявлена именно таким образом (возвращает константный результат по значению и принимает ссылку на const в качестве аргумента), обратитесь к правилам [[Везде, где только можно используйте const | 3]], [[Правило 20: Предпочитайте передачу по ссылке на const передаче по значению | 20]] и [[Правило 21: Не пытайтесь вернуть ссылку, когда должны вернуть объект | 21]].</P> <P>Такое решение позволяет легко манипулировать рациональными числами:</P> <source lang="cpp"> Rational oneEighth(1, 8); Rational one Half(1, 2); Rational result = oneHalf * oneEighth; // правильно result = result * oneEighth; // правильно </source> <P>Но вы не удовлетворены. Хотелось бы поддерживать также смешанные операции, чтобы Rational можно было умножить, например, на int. В конце концов, это довольно естественно – иметь возможность перемножать два числа, даже если они принадлежат к разным числовым типам.</P> <P>Однако если вы попытаетесь выполнить смешанные арифметические операции, то обнаружите, что они работают только в половине случаев:</P> <source lang="cpp"> result = oneHalf * 2; // правильно result = 2 * oneHalf; // ошибка! </source> <P>Это плохой знак. Умножение должно быть коммутативным (не зависеть от порядка сомножителей), помните?</P> <P>Источник проблемы становится понятным, если переписать два последних выражения в функциональной форме:</P> <source lang="cpp"> result = oneHalf.operator*(2); // правильно result = 2.operator*(oneHalf); // ошибка! </source> <P>Объект oneHalf – это экземпляр класса, включающего в себя operator*, поэтому компилятор вызывает эту функцию. Но с целым числом 2 не ассоциирован никакой класс, а значит, нет для него и функции operator*. Компилятор будет также искать функции operator*, не являющиеся членами класса (в текущем пространстве имен или в глобальной области видимости):</P> <source lang="cpp"> result = operator*(2, oneHalf); // ошибка! </source> <P>Но в данном случае нет и свободной функции operator*, которая принимала бы аргументы int и Rational, поэтому поиск завершится ничем.</P> <P>Посмотрим еще раз на успешный вызов. Видите, что второй параметр – целое число 2, хотя Rational::operator* принимает в качестве аргумента объект Rational. Что происходит? Почему 2 работает в одной позиции и не работает в другой?</P> <P>Происходит неявное преобразование типа. Компилятор знает, что вы передали int, а функция требует Rational, но он также знает, что можно получить подходящий объект, если вызвать конструктор Rational c переданным вами аргументом int. Так он и поступает. Иными словами, компилятор трактует показанный выше вызов, как если бы он был написан примерно так:</P> <source lang="cpp"> const Rational temp(2); // создать временный объект Rational из 2 result = oneHalf * temp; // то же, что oneHalf.operator*(temp); </source> <P>Конечно, компилятор делает это только потому, что есть конструктор, объявленный без квалификатора explicit. Если бы квалификатор explicit присутствовал, то ни одно из следующих предложений не скомпилировалось бы:</P> <source lang="cpp"> result = oneHalf * 2; // ошибка! (при наличии explicit-конструктора): // невозможно преобразовать 2 в Ratinal result = 2 * oneHalf; // та же ошибка, та же проблема </source> <P>Со смешанной арифметикой при таком подходе придется распроститься, но, по крайней мере, такое поведение непротиворечиво.</P> <P>Ваша цель, однако, – обеспечить и согласованность, и поддержку смешанной арифметики, то есть нужно найти такое решение, при котором оба предложения компилируются. Это возвращает нас к вопросу о том, почему даже при наличии explicit-конструктора в классе Rational одно из них компилируется, а другое – нет:</P> <source lang="cpp"> result = oneHalf * 2; // ошибка! (при наличии explicit-конструктора): // невозможно преобразовать 2 в Ratinal result = 2 * oneHalf; // та же ошибка, та же проблема </source> <P>Оказывается, что к параметрам применимы неявные преобразования, <EM>только если они перечислены в списке параметров.</EM> Неявный параметр, соответствующий объекту, чья функция-член вызывается (тот, на который указывает this), никогда не подвергается неявному преобразованию. Вот почему первый вызов компилируется, а второй – нет. В первом случае параметр указан в списке параметров функции, а во втором – нет.</P> <P>Однако вам хотелось бы получить полноценную поддержку смешанной арифметики, и теперь ясно, как ее обеспечить: нужен operator* в виде свободной функции, тогда компилятор сможет выполнить неявное преобразование <EM>всех</EM> аргументов:</P> <source lang="cpp"> class Rational { ... // не содержит operator* }; const Rational operator*(const Rational& lhs, // теперь свободная функция const Rational& rhs) { return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); } Rational oneFourth(1, 4); Rational result; result = oneFourth * 2; // правильно result = 2 * oneFourth; // ура, работает! </source> <P>Это можно было бы назвать счастливым концом, если бы не одно «но». Должен ли operator* быть другом класса Rational?</P> <P>В данном случае ответом будет «нет», потому что operator* может быть реализован полностью в терминах открытого интерфейса Rational. Приведенный выше код показывает, как это можно сделать. И мы приходим к важному выводу: противоположностью функции-члена является свободная функция, а функция – друг класса. Многие программисты на C++ полагают, что раз функция имеет отношение к классу и не должна быть его членом (например, из-за необходимости преобразовывать типы всех аргументов), то она должна быть другом. Этот пример показывает, что такое предположение неправильно. Если вы можете избежать назначения функции другом класса, то должны так и поступить, потому что, как и в реальной жизни, друзья часто доставляют больше хлопот, чем хотелось бы. Конечно, иногда отношения дружественности оправданы, но факт остается фактом: если функция не должна быть членом, это не означает автоматически, что она должна быть другом.</P> <P>Сказанное выше правда, и ничего, кроме правды, но это не вся правда. Когда вы переходите от «Объектно-ориентированного C++» к «C++ с шаблонами» (см. правило 1) и превращаете Rational из класса в <EM>шаблон класса,</EM> то вступают в силу новые факторы, новые способы их учета, и появляются неожиданные проектные решения. Все это является темой [[Правило 46: Определяйте внутри шаблонов функции, не являющиеся членами, когда желательны преобразования типа | правила 46]].</P> == Что следует помнить == *Если преобразование типов должно быть применимо ко всем параметрам функции (включая и скрытый параметр this), то функция не должна быть членом класса. 6e9c08b66e07e0db23be58969e4109b45c0d8665 48 47 2013-06-26T13:16:42Z Lerom 3360334 wikitext text/x-wiki <P>Во введении я отмечал, что в общем случае поддержка классом неявных преобразований типов – неудачная мысль. Но, конечно, из этого правила есть исключения, и одно из наиболее важных касается создания числовых типов. Например, если вы проектируете класс для представления рациональных чисел, то неявное преобразование целого числа в рациональное выглядит вполне разумно. Уж во всяком случае не менее разумно, чем встроенное в C++ преобразование int в double (и куда разумнее встроенного преобразования из double в int). Коли так, то начать объявления класса Rational можно было бы следующим образом:</P> <source lang="cpp"> class Rational { public: Rational(int numerator = 0, int denominator = 1); // конструктор сознательно не explicit; // допускает неявное преобразование // int в Rational int numerator() const; // функции доступа к числителю и int denominator() const; // знаменателю – см. правило 22 private: ... }; </source> <P>Вы знаете, что понадобится поддерживать арифметические операции (сложение, умножение и т. п.), но не уверены, следует реализовывать их посредством функций-членов или свободных функций, возможно, являющихся друзьями класса. Инстинкт говорит: «Сомневаешься – придерживайся объектно-ориентированного подхода». Вы понимаете, что, скажем, умножение рациональных чисел относится к классу Rational, поэтому кажется естественным реализовать operator* в самом этом классе. Но наперекор интуиции [[Правило 23: Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса | правило 23]] утверждает, что идея помещения функции внутрь класса, с которым она ассоциирована, иногда противоречит объектно-ориентированным принципам. Впрочем, оставим на время эту тему и посмотрим, во что выливается объявление operator* функцией-членом Rational:</P> <source lang="cpp"> class Rational { public: ... const Rational operator*(const Rational& rhs) const; } </source> <P>Если вы не понимаете, почему эта функция объявлена именно таким образом (возвращает константный результат по значению и принимает ссылку на const в качестве аргумента), обратитесь к правилам [[Везде, где только можно используйте const | 3]], [[Правило 20: Предпочитайте передачу по ссылке на const передаче по значению | 20]] и [[Правило 21: Не пытайтесь вернуть ссылку, когда должны вернуть объект | 21]].</P> <P>Такое решение позволяет легко манипулировать рациональными числами:</P> <source lang="cpp"> Rational oneEighth(1, 8); Rational one Half(1, 2); Rational result = oneHalf * oneEighth; // правильно result = result * oneEighth; // правильно </source> <P>Но вы не удовлетворены. Хотелось бы поддерживать также смешанные операции, чтобы Rational можно было умножить, например, на int. В конце концов, это довольно естественно – иметь возможность перемножать два числа, даже если они принадлежат к разным числовым типам.</P> <P>Однако если вы попытаетесь выполнить смешанные арифметические операции, то обнаружите, что они работают только в половине случаев:</P> <source lang="cpp"> result = oneHalf * 2; // правильно result = 2 * oneHalf; // ошибка! </source> <P>Это плохой знак. Умножение должно быть коммутативным (не зависеть от порядка сомножителей), помните?</P> <P>Источник проблемы становится понятным, если переписать два последних выражения в функциональной форме:</P> <source lang="cpp"> result = oneHalf.operator*(2); // правильно result = 2.operator*(oneHalf); // ошибка! </source> <P>Объект oneHalf – это экземпляр класса, включающего в себя operator*, поэтому компилятор вызывает эту функцию. Но с целым числом 2 не ассоциирован никакой класс, а значит, нет для него и функции operator*. Компилятор будет также искать функции operator*, не являющиеся членами класса (в текущем пространстве имен или в глобальной области видимости):</P> <source lang="cpp"> result = operator*(2, oneHalf); // ошибка! </source> <P>Но в данном случае нет и свободной функции operator*, которая принимала бы аргументы int и Rational, поэтому поиск завершится ничем.</P> <P>Посмотрим еще раз на успешный вызов. Видите, что второй параметр – целое число 2, хотя Rational::operator* принимает в качестве аргумента объект Rational. Что происходит? Почему 2 работает в одной позиции и не работает в другой?</P> <P>Происходит неявное преобразование типа. Компилятор знает, что вы передали int, а функция требует Rational, но он также знает, что можно получить подходящий объект, если вызвать конструктор Rational c переданным вами аргументом int. Так он и поступает. Иными словами, компилятор трактует показанный выше вызов, как если бы он был написан примерно так:</P> <source lang="cpp"> const Rational temp(2); // создать временный объект Rational из 2 result = oneHalf * temp; // то же, что oneHalf.operator*(temp); </source> <P>Конечно, компилятор делает это только потому, что есть конструктор, объявленный без квалификатора explicit. Если бы квалификатор explicit присутствовал, то ни одно из следующих предложений не скомпилировалось бы:</P> <source lang="cpp"> result = oneHalf * 2; // ошибка! (при наличии explicit-конструктора): // невозможно преобразовать 2 в Ratinal result = 2 * oneHalf; // та же ошибка, та же проблема </source> <P>Со смешанной арифметикой при таком подходе придется распроститься, но, по крайней мере, такое поведение непротиворечиво.</P> <P>Ваша цель, однако, – обеспечить и согласованность, и поддержку смешанной арифметики, то есть нужно найти такое решение, при котором оба предложения компилируются. Это возвращает нас к вопросу о том, почему даже при наличии explicit-конструктора в классе Rational одно из них компилируется, а другое – нет:</P> <source lang="cpp"> result = oneHalf * 2; // ошибка! (при наличии explicit-конструктора): // невозможно преобразовать 2 в Ratinal result = 2 * oneHalf; // та же ошибка, та же проблема </source> <P>Оказывается, что к параметрам применимы неявные преобразования, <EM>только если они перечислены в списке параметров.</EM> Неявный параметр, соответствующий объекту, чья функция-член вызывается (тот, на который указывает this), никогда не подвергается неявному преобразованию. Вот почему первый вызов компилируется, а второй – нет. В первом случае параметр указан в списке параметров функции, а во втором – нет.</P> <P>Однако вам хотелось бы получить полноценную поддержку смешанной арифметики, и теперь ясно, как ее обеспечить: нужен operator* в виде свободной функции, тогда компилятор сможет выполнить неявное преобразование <EM>всех</EM> аргументов:</P> <source lang="cpp"> class Rational { ... // не содержит operator* }; const Rational operator*(const Rational& lhs, // теперь свободная функция const Rational& rhs) { return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); } Rational oneFourth(1, 4); Rational result; result = oneFourth * 2; // правильно result = 2 * oneFourth; // ура, работает! </source> <P>Это можно было бы назвать счастливым концом, если бы не одно «но». Должен ли operator* быть другом класса Rational?</P> <P>В данном случае ответом будет «нет», потому что operator* может быть реализован полностью в терминах открытого интерфейса Rational. Приведенный выше код показывает, как это можно сделать. И мы приходим к важному выводу: противоположностью функции-члена является свободная функция, а функция – друг класса. Многие программисты на C++ полагают, что раз функция имеет отношение к классу и не должна быть его членом (например, из-за необходимости преобразовывать типы всех аргументов), то она должна быть другом. Этот пример показывает, что такое предположение неправильно. Если вы можете избежать назначения функции другом класса, то должны так и поступить, потому что, как и в реальной жизни, друзья часто доставляют больше хлопот, чем хотелось бы. Конечно, иногда отношения дружественности оправданы, но факт остается фактом: если функция не должна быть членом, это не означает автоматически, что она должна быть другом.</P> <P>Сказанное выше правда, и ничего, кроме правды, но это не вся правда. Когда вы переходите от «Объектно-ориентированного C++» к «C++ с шаблонами» ([[Правило 1: Относитесь к C++ как к конгломерату языков | см. правило 1]]) и превращаете Rational из класса в <EM>шаблон класса,</EM> то вступают в силу новые факторы, новые способы их учета, и появляются неожиданные проектные решения. Все это является темой [[Правило 46: Определяйте внутри шаблонов функции, не являющиеся членами, когда желательны преобразования типа | правила 46]].</P> == Что следует помнить == *Если преобразование типов должно быть применимо ко всем параметрам функции (включая и скрытый параметр this), то функция не должна быть членом класса. 62ec9262dc36bb9e3534b5e6cb0d9a78ac07d721 49 48 2013-06-26T13:17:55Z Lerom 3360334 wikitext text/x-wiki <P>Во введении я отмечал, что в общем случае поддержка классом неявных преобразований типов – неудачная мысль. Но, конечно, из этого правила есть исключения, и одно из наиболее важных касается создания числовых типов. Например, если вы проектируете класс для представления рациональных чисел, то неявное преобразование целого числа в рациональное выглядит вполне разумно. Уж во всяком случае не менее разумно, чем встроенное в C++ преобразование int в double (и куда разумнее встроенного преобразования из double в int). Коли так, то начать объявления класса Rational можно было бы следующим образом:</P> <source lang="cpp"> class Rational { public: Rational(int numerator = 0, int denominator = 1); // конструктор сознательно не explicit; // допускает неявное преобразование // int в Rational int numerator() const; // функции доступа к числителю и int denominator() const; // знаменателю – см. правило 22 private: ... }; </source> <P>Вы знаете, что понадобится поддерживать арифметические операции (сложение, умножение и т. п.), но не уверены, следует реализовывать их посредством функций-членов или свободных функций, возможно, являющихся друзьями класса. Инстинкт говорит: «Сомневаешься – придерживайся объектно-ориентированного подхода». Вы понимаете, что, скажем, умножение рациональных чисел относится к классу Rational, поэтому кажется естественным реализовать operator* в самом этом классе. Но наперекор интуиции [[Правило 23: Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса | правило 23]] утверждает, что идея помещения функции внутрь класса, с которым она ассоциирована, иногда противоречит объектно-ориентированным принципам. Впрочем, оставим на время эту тему и посмотрим, во что выливается объявление operator* функцией-членом Rational:</P> <source lang="cpp"> class Rational { public: ... const Rational operator*(const Rational& rhs) const; } </source> <P>Если вы не понимаете, почему эта функция объявлена именно таким образом (возвращает константный результат по значению и принимает ссылку на const в качестве аргумента), обратитесь к правилам [[Правило 3: Везде, где только можно используйте const | 3]], [[Правило 20: Предпочитайте передачу по ссылке на const передаче по значению | 20]] и [[Правило 21: Не пытайтесь вернуть ссылку, когда должны вернуть объект | 21]].</P> <P>Такое решение позволяет легко манипулировать рациональными числами:</P> <source lang="cpp"> Rational oneEighth(1, 8); Rational one Half(1, 2); Rational result = oneHalf * oneEighth; // правильно result = result * oneEighth; // правильно </source> <P>Но вы не удовлетворены. Хотелось бы поддерживать также смешанные операции, чтобы Rational можно было умножить, например, на int. В конце концов, это довольно естественно – иметь возможность перемножать два числа, даже если они принадлежат к разным числовым типам.</P> <P>Однако если вы попытаетесь выполнить смешанные арифметические операции, то обнаружите, что они работают только в половине случаев:</P> <source lang="cpp"> result = oneHalf * 2; // правильно result = 2 * oneHalf; // ошибка! </source> <P>Это плохой знак. Умножение должно быть коммутативным (не зависеть от порядка сомножителей), помните?</P> <P>Источник проблемы становится понятным, если переписать два последних выражения в функциональной форме:</P> <source lang="cpp"> result = oneHalf.operator*(2); // правильно result = 2.operator*(oneHalf); // ошибка! </source> <P>Объект oneHalf – это экземпляр класса, включающего в себя operator*, поэтому компилятор вызывает эту функцию. Но с целым числом 2 не ассоциирован никакой класс, а значит, нет для него и функции operator*. Компилятор будет также искать функции operator*, не являющиеся членами класса (в текущем пространстве имен или в глобальной области видимости):</P> <source lang="cpp"> result = operator*(2, oneHalf); // ошибка! </source> <P>Но в данном случае нет и свободной функции operator*, которая принимала бы аргументы int и Rational, поэтому поиск завершится ничем.</P> <P>Посмотрим еще раз на успешный вызов. Видите, что второй параметр – целое число 2, хотя Rational::operator* принимает в качестве аргумента объект Rational. Что происходит? Почему 2 работает в одной позиции и не работает в другой?</P> <P>Происходит неявное преобразование типа. Компилятор знает, что вы передали int, а функция требует Rational, но он также знает, что можно получить подходящий объект, если вызвать конструктор Rational c переданным вами аргументом int. Так он и поступает. Иными словами, компилятор трактует показанный выше вызов, как если бы он был написан примерно так:</P> <source lang="cpp"> const Rational temp(2); // создать временный объект Rational из 2 result = oneHalf * temp; // то же, что oneHalf.operator*(temp); </source> <P>Конечно, компилятор делает это только потому, что есть конструктор, объявленный без квалификатора explicit. Если бы квалификатор explicit присутствовал, то ни одно из следующих предложений не скомпилировалось бы:</P> <source lang="cpp"> result = oneHalf * 2; // ошибка! (при наличии explicit-конструктора): // невозможно преобразовать 2 в Ratinal result = 2 * oneHalf; // та же ошибка, та же проблема </source> <P>Со смешанной арифметикой при таком подходе придется распроститься, но, по крайней мере, такое поведение непротиворечиво.</P> <P>Ваша цель, однако, – обеспечить и согласованность, и поддержку смешанной арифметики, то есть нужно найти такое решение, при котором оба предложения компилируются. Это возвращает нас к вопросу о том, почему даже при наличии explicit-конструктора в классе Rational одно из них компилируется, а другое – нет:</P> <source lang="cpp"> result = oneHalf * 2; // ошибка! (при наличии explicit-конструктора): // невозможно преобразовать 2 в Ratinal result = 2 * oneHalf; // та же ошибка, та же проблема </source> <P>Оказывается, что к параметрам применимы неявные преобразования, <EM>только если они перечислены в списке параметров.</EM> Неявный параметр, соответствующий объекту, чья функция-член вызывается (тот, на который указывает this), никогда не подвергается неявному преобразованию. Вот почему первый вызов компилируется, а второй – нет. В первом случае параметр указан в списке параметров функции, а во втором – нет.</P> <P>Однако вам хотелось бы получить полноценную поддержку смешанной арифметики, и теперь ясно, как ее обеспечить: нужен operator* в виде свободной функции, тогда компилятор сможет выполнить неявное преобразование <EM>всех</EM> аргументов:</P> <source lang="cpp"> class Rational { ... // не содержит operator* }; const Rational operator*(const Rational& lhs, // теперь свободная функция const Rational& rhs) { return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); } Rational oneFourth(1, 4); Rational result; result = oneFourth * 2; // правильно result = 2 * oneFourth; // ура, работает! </source> <P>Это можно было бы назвать счастливым концом, если бы не одно «но». Должен ли operator* быть другом класса Rational?</P> <P>В данном случае ответом будет «нет», потому что operator* может быть реализован полностью в терминах открытого интерфейса Rational. Приведенный выше код показывает, как это можно сделать. И мы приходим к важному выводу: противоположностью функции-члена является свободная функция, а функция – друг класса. Многие программисты на C++ полагают, что раз функция имеет отношение к классу и не должна быть его членом (например, из-за необходимости преобразовывать типы всех аргументов), то она должна быть другом. Этот пример показывает, что такое предположение неправильно. Если вы можете избежать назначения функции другом класса, то должны так и поступить, потому что, как и в реальной жизни, друзья часто доставляют больше хлопот, чем хотелось бы. Конечно, иногда отношения дружественности оправданы, но факт остается фактом: если функция не должна быть членом, это не означает автоматически, что она должна быть другом.</P> <P>Сказанное выше правда, и ничего, кроме правды, но это не вся правда. Когда вы переходите от «Объектно-ориентированного C++» к «C++ с шаблонами» ([[Правило 1: Относитесь к C++ как к конгломерату языков | см. правило 1]]) и превращаете Rational из класса в <EM>шаблон класса,</EM> то вступают в силу новые факторы, новые способы их учета, и появляются неожиданные проектные решения. Все это является темой [[Правило 46: Определяйте внутри шаблонов функции, не являющиеся членами, когда желательны преобразования типа | правила 46]].</P> == Что следует помнить == *Если преобразование типов должно быть применимо ко всем параметрам функции (включая и скрытый параметр this), то функция не должна быть членом класса. 890f03271fcd94eab25aeb9cfc72dc0550130bb1 Заглавная страница 0 1 51 50 2013-06-26T13:21:34Z Lerom 3360334 wikitext text/x-wiki == Скотт Мэйерс. Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ == ==== Глава 1. Приучайтесь к C++ ==== [[Правило 1: Относитесь к C++ как к конгломерату языков]]<br> [[Правило 2: Предпочитайте const, enum и inline использованию #define]]<br> [[Правило 3: Везде, где только можно используйте const]]<br> :[[Правило 3: Везде, где только можно используйте const#Константные функции-члены | Константные функции-члены]]<br> :[[Правило 3: Везде, где только можно используйте const#Как избежать дублирования в константных и неконстантных функциях-членах | Как избежать дублирования в константных и неконстантных функциях-членах]]<br> [[Правило 4: Прежде чем использовать объекты, убедитесь, что они инициализированы]]<br> ==== Глава 2. Конструкторы, деструкторы и операторы присваивания ==== [[Правило 5: Какие функции C++ создает и вызывает молча]]<br> [[Правило 6: Явно запрещайте компилятору генерировать функции, которые вам не нужны]]<br> [[Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе]]<br> [[Правило 8: Не позволяйте исключениям покидать деструкторы]]<br> [[Правило 9: Никогда не вызывайте виртуальные функции в конструкторе или деструкторе]]<br> [[Правило 10: Операторы присваивания должны возвращать ссылку на *this]]<br> [[Правило 11: В operator= осуществляйте проверку на присваивание самому себе]]<br> [[Правило 12: Копируйте все части объекта]]<br> ==== Глава 3 Управление ресурсами ==== [[Правило 13: Используйте объекты для управления ресурсами]]<br> [[Правило 14: Тщательно продумывайте поведение при копировании классов, управляющих ресурсами]]<br> [[Правило 15: Предоставляйте доступ к самим ресурсам из управляющих ими классов]]<br> [[Правило 16: Используйте одинаковые формы new и delete]]<br> [[Правило 17: Помещение в «интеллектуальный» указатель объекта, вьщеленного с помощью new, лучше располагать в отдельном предложении]]<br> ==== Глава 4 Проектирование программ и объявления ==== [[Правило 18: Проектируйте интерфейсы так, что их легко было использовать правильно и трудно – неправильно]]<br> [[Правило 19: Рассматривайте проектирование класса как проектирование типа]]<br> [[Правило 20: Предпочитайте передачу по ссылке на const передаче по значению]]<br> [[Правило 21: Не пытайтесь вернуть ссылку, когда должны вернуть объект]]<br> [[Правило 22: Объявляйте данные-члены закрытыми]]<br> [[Правило 23: Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса]]<br> [[Правило 24: Объявляйте функции, не являющиеся членами, когда преобразование типов должно быть применимо ко всем параметрам]]<br> [[Правило 25: Подумайте о поддержке функции swap, не возбуждающей исключений]]<br> ==== Глава 5 Реализация ==== [[Правило 26: Откладывайте определение переменных насколько возможно]]<br> [[Правило 27: Не злоупотребляйте приведением типов]]<br> [[Правило 28: Избегайте возвращения «дескрипторов» внутренних данных]]<br> [[Правило 29: Стремитесь, чтобы программа была безопасна относительно исключений]]<br> [[Правило 30: Тщательно обдумывайте использование встроенных функций]]<br> [[Правило 31: Уменьшайте зависимости файлов при компиляции]]<br> ==== Глава 6 Наследование и объектно-ориентированное проектирование ==== [[Правило 32: Используйте открытое наследование для моделирования отношения «является»]]v [[Правило 33: Не скрывайте унаследованные имена]]<br> [[Правило 34: Различайте наследование интерфейса и наследование реализации]]<br> [[Правило 35: Рассмотрите альтернативы виртуальным функциям]]<br> :[[Реализация паттерна««Шаблонный метод» с помощью идиомы невиртуального интерфейса]]<br> :[[Реализация паттерна «Стратегия» посредством указателей на функции]]<br> :[[Реализация паттерна «Стратегия» посредством класса tr::function]]<br> :[[«Классический» паттерн «Стратегия»]]<br> :[[Резюме]]<br> [[Правило 36: Никогда не переопределяйте наследуемые невиртуальные функции]]<br> [[Правило 37: Никогда не переопределяйте наследуемое значение аргумента функции по умолчанию]]<br> [[Правило 38: Моделируйте отношение «содержит» или «реализуется посредством» с помощью композиции]]<br> [[Правило 39: Продумывайте подход к использованию закрытого наследования]]<br> [[Правило 40: Продумывайте подход к использованию множественного наследования]]<br> ==== Глава 7 Шаблоны и обобщенное программирование ==== [[Правило 41: Разберитесь в том, что такое неявные интерфейсы и полиморфизм на этапе компиляции]]<br> [[Правило 42: Усвойте оба значения ключевого слова typename]]<br> [[Правило 43: Необходимо знать, как обращаться к именам в шаблонных базовых классах]]<br> [[Правило 44: Размещайте независимый от параметров код вне шаблонов]]<br> [[Правило 45: Разрабатывайте шаблоны функций-членов так, чтобы они принимали «все совместимые типы»]]<br> [[Правило 46: Определяйте внутри шаблонов функции, не являющиеся членами, когда желательны преобразования типа]]<br> [[Правило 47: Используйте классы-характеристики для предоставления информации о типах]]<br> [[Правило 48: Изучите метапрограммирование шаблонов]]<br> ==== Глава 8 Настройка new и delete ==== [[Правило 49: Разберитесь в поведении обработчика new]]<br> [[Правило 50: Когда имеет смысл заменять new и delete]]<br> [[Правило 51: Придерживайтесь принятых соглашений при написании new и delete]]<br> [[Правило 52: Если вы написали оператор new с размещением, напишите и соответствующий оператор delete]]<br> ==== Глава 9 Разное ==== [[Правило 53: Обращайте внимание на предупреждения компилятора]]<br> [[Правило 54: Ознакомьтесь со стандартной библиотекой, включая TR1]]<br> [[Правило 55: Познакомьтесь с Boost]]<br> 84aba3a020e0ef2903e2adf56357a20863d51726 52 51 2013-06-26T13:22:09Z Lerom 3360334 wikitext text/x-wiki == Скотт Мэйерс. Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ == ==== Глава 1. Приучайтесь к C++ ==== [[Правило 1: Относитесь к C++ как к конгломерату языков]]<br> [[Правило 2: Предпочитайте const, enum и inline использованию #define]]<br> [[Правило 3: Везде, где только можно используйте const]]<br> :[[Правило 3: Везде, где только можно используйте const#Константные функции-члены | Константные функции-члены]]<br> :[[Правило 3: Везде, где только можно используйте const#Как избежать дублирования в константных и неконстантных функциях-членах | Как избежать дублирования в константных и неконстантных функциях-членах]]<br> [[Правило 4: Прежде чем использовать объекты, убедитесь, что они инициализированы]]<br> ==== Глава 2. Конструкторы, деструкторы и операторы присваивания ==== [[Правило 5: Какие функции C++ создает и вызывает молча]]<br> [[Правило 6: Явно запрещайте компилятору генерировать функции, которые вам не нужны]]<br> [[Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе]]<br> [[Правило 8: Не позволяйте исключениям покидать деструкторы]]<br> [[Правило 9: Никогда не вызывайте виртуальные функции в конструкторе или деструкторе]]<br> [[Правило 10: Операторы присваивания должны возвращать ссылку на *this]]<br> [[Правило 11: В operator= осуществляйте проверку на присваивание самому себе]]<br> [[Правило 12: Копируйте все части объекта]]<br> ==== Глава 3 Управление ресурсами ==== [[Правило 13: Используйте объекты для управления ресурсами]]<br> [[Правило 14: Тщательно продумывайте поведение при копировании классов, управляющих ресурсами]]<br> [[Правило 15: Предоставляйте доступ к самим ресурсам из управляющих ими классов]]<br> [[Правило 16: Используйте одинаковые формы new и delete]]<br> [[Правило 17: Помещение в «интеллектуальный» указатель объекта, вьщеленного с помощью new, лучше располагать в отдельном предложении]]<br> ==== Глава 4 Проектирование программ и объявления ==== [[Правило 18: Проектируйте интерфейсы так, что их легко было использовать правильно и трудно – неправильно]]<br> [[Правило 19: Рассматривайте проектирование класса как проектирование типа]]<br> [[Правило 20: Предпочитайте передачу по ссылке на const передаче по значению]]<br> [[Правило 21: Не пытайтесь вернуть ссылку, когда должны вернуть объект]]<br> [[Правило 22: Объявляйте данные-члены закрытыми]]<br> [[Правило 23: Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса]]<br> [[Правило 24: Объявляйте функции, не являющиеся членами, когда преобразование типов должно быть применимо ко всем параметрам]]<br> [[Правило 25: Подумайте о поддержке функции swap, не возбуждающей исключений]]<br> ==== Глава 5 Реализация ==== [[Правило 26: Откладывайте определение переменных насколько возможно]]<br> [[Правило 27: Не злоупотребляйте приведением типов]]<br> [[Правило 28: Избегайте возвращения «дескрипторов» внутренних данных]]<br> [[Правило 29: Стремитесь, чтобы программа была безопасна относительно исключений]]<br> [[Правило 30: Тщательно обдумывайте использование встроенных функций]]<br> [[Правило 31: Уменьшайте зависимости файлов при компиляции]]<br> ==== Глава 6 Наследование и объектно-ориентированное проектирование ==== [[Правило 32: Используйте открытое наследование для моделирования отношения «является»]]v [[Правило 33: Не скрывайте унаследованные имена]]<br> [[Правило 34: Различайте наследование интерфейса и наследование реализации]]<br> [[Правило 35: Рассмотрите альтернативы виртуальным функциям]]<br> :[[Реализация паттерна««Шаблонный метод» с помощью идиомы невиртуального интерфейса]]<br> :[[Реализация паттерна «Стратегия» посредством указателей на функции]]<br> :[[Реализация паттерна «Стратегия» посредством класса tr::function]]<br> :[[«Классический» паттерн «Стратегия»]]<br> :[[Резюме]]<br> [[Правило 36: Никогда не переопределяйте наследуемые невиртуальные функции]]<br> [[Правило 37: Никогда не переопределяйте наследуемое значение аргумента функции по умолчанию]]<br> [[Правило 38: Моделируйте отношение «содержит» или «реализуется посредством» с помощью композиции]]<br> [[Правило 39: Продумывайте подход к использованию закрытого наследования]]<br> [[Правило 40: Продумывайте подход к использованию множественного наследования]]<br> ==== Глава 7 Шаблоны и обобщенное программирование ==== [[Правило 41: Разберитесь в том, что такое неявные интерфейсы и полиморфизм на этапе компиляции]]<br> [[Правило 42: Усвойте оба значения ключевого слова typename]]<br> [[Правило 43: Необходимо знать, как обращаться к именам в шаблонных базовых классах]]<br> [[Правило 44: Размещайте независимый от параметров код вне шаблонов]]<br> [[Правило 45: Разрабатывайте шаблоны функций-членов так, чтобы они принимали «все совместимые типы»]]<br> [[Правило 46: Определяйте внутри шаблонов функции, не являющиеся членами, когда желательны преобразования типа]]<br> [[Правило 47: Используйте классы-характеристики для предоставления информации о типах]]<br> [[Правило 48: Изучите метапрограммирование шаблонов]]<br> ==== Глава 8 Настройка new и delete ==== [[Правило 49: Разберитесь в поведении обработчика new]]<br> [[Правило 50: Когда имеет смысл заменять new и delete]]<br> [[Правило 51: Придерживайтесь принятых соглашений при написании new и delete]]<br> [[Правило 52: Если вы написали оператор new с размещением, напишите и соответствующий оператор delete]]<br> ==== Глава 9 Разное ==== [[Правило 53: Обращайте внимание на предупреждения компилятора]]<br> [[Правило 54: Ознакомьтесь со стандартной библиотекой, включая TR1]]<br> [[Правило 55: Познакомьтесь с Boost]]<br> 00cedcef79bd7f2c6d9b8b74f5ad2b7e6ff7e9b0 57 52 2013-07-08T12:54:33Z Lerom 3360334 /* Глава 6 Наследование и объектно-ориентированное проектирование */ wikitext text/x-wiki == Скотт Мэйерс. Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ == ==== Глава 1. Приучайтесь к C++ ==== [[Правило 1: Относитесь к C++ как к конгломерату языков]]<br> [[Правило 2: Предпочитайте const, enum и inline использованию #define]]<br> [[Правило 3: Везде, где только можно используйте const]]<br> :[[Правило 3: Везде, где только можно используйте const#Константные функции-члены | Константные функции-члены]]<br> :[[Правило 3: Везде, где только можно используйте const#Как избежать дублирования в константных и неконстантных функциях-членах | Как избежать дублирования в константных и неконстантных функциях-членах]]<br> [[Правило 4: Прежде чем использовать объекты, убедитесь, что они инициализированы]]<br> ==== Глава 2. Конструкторы, деструкторы и операторы присваивания ==== [[Правило 5: Какие функции C++ создает и вызывает молча]]<br> [[Правило 6: Явно запрещайте компилятору генерировать функции, которые вам не нужны]]<br> [[Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе]]<br> [[Правило 8: Не позволяйте исключениям покидать деструкторы]]<br> [[Правило 9: Никогда не вызывайте виртуальные функции в конструкторе или деструкторе]]<br> [[Правило 10: Операторы присваивания должны возвращать ссылку на *this]]<br> [[Правило 11: В operator= осуществляйте проверку на присваивание самому себе]]<br> [[Правило 12: Копируйте все части объекта]]<br> ==== Глава 3 Управление ресурсами ==== [[Правило 13: Используйте объекты для управления ресурсами]]<br> [[Правило 14: Тщательно продумывайте поведение при копировании классов, управляющих ресурсами]]<br> [[Правило 15: Предоставляйте доступ к самим ресурсам из управляющих ими классов]]<br> [[Правило 16: Используйте одинаковые формы new и delete]]<br> [[Правило 17: Помещение в «интеллектуальный» указатель объекта, вьщеленного с помощью new, лучше располагать в отдельном предложении]]<br> ==== Глава 4 Проектирование программ и объявления ==== [[Правило 18: Проектируйте интерфейсы так, что их легко было использовать правильно и трудно – неправильно]]<br> [[Правило 19: Рассматривайте проектирование класса как проектирование типа]]<br> [[Правило 20: Предпочитайте передачу по ссылке на const передаче по значению]]<br> [[Правило 21: Не пытайтесь вернуть ссылку, когда должны вернуть объект]]<br> [[Правило 22: Объявляйте данные-члены закрытыми]]<br> [[Правило 23: Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса]]<br> [[Правило 24: Объявляйте функции, не являющиеся членами, когда преобразование типов должно быть применимо ко всем параметрам]]<br> [[Правило 25: Подумайте о поддержке функции swap, не возбуждающей исключений]]<br> ==== Глава 5 Реализация ==== [[Правило 26: Откладывайте определение переменных насколько возможно]]<br> [[Правило 27: Не злоупотребляйте приведением типов]]<br> [[Правило 28: Избегайте возвращения «дескрипторов» внутренних данных]]<br> [[Правило 29: Стремитесь, чтобы программа была безопасна относительно исключений]]<br> [[Правило 30: Тщательно обдумывайте использование встроенных функций]]<br> [[Правило 31: Уменьшайте зависимости файлов при компиляции]]<br> ==== Глава 6 Наследование и объектно-ориентированное проектирование ==== [[Правило 32: Используйте открытое наследование для моделирования отношения «является»]] [[Правило 33: Не скрывайте унаследованные имена]]<br> [[Правило 34: Различайте наследование интерфейса и наследование реализации]]<br> [[Правило 35: Рассмотрите альтернативы виртуальным функциям]]<br> :[[Реализация паттерна««Шаблонный метод» с помощью идиомы невиртуального интерфейса]]<br> :[[Реализация паттерна «Стратегия» посредством указателей на функции]]<br> :[[Реализация паттерна «Стратегия» посредством класса tr::function]]<br> :[[«Классический» паттерн «Стратегия»]]<br> :[[Резюме]]<br> [[Правило 36: Никогда не переопределяйте наследуемые невиртуальные функции]]<br> [[Правило 37: Никогда не переопределяйте наследуемое значение аргумента функции по умолчанию]]<br> [[Правило 38: Моделируйте отношение «содержит» или «реализуется посредством» с помощью композиции]]<br> [[Правило 39: Продумывайте подход к использованию закрытого наследования]]<br> [[Правило 40: Продумывайте подход к использованию множественного наследования]]<br> ==== Глава 7 Шаблоны и обобщенное программирование ==== [[Правило 41: Разберитесь в том, что такое неявные интерфейсы и полиморфизм на этапе компиляции]]<br> [[Правило 42: Усвойте оба значения ключевого слова typename]]<br> [[Правило 43: Необходимо знать, как обращаться к именам в шаблонных базовых классах]]<br> [[Правило 44: Размещайте независимый от параметров код вне шаблонов]]<br> [[Правило 45: Разрабатывайте шаблоны функций-членов так, чтобы они принимали «все совместимые типы»]]<br> [[Правило 46: Определяйте внутри шаблонов функции, не являющиеся членами, когда желательны преобразования типа]]<br> [[Правило 47: Используйте классы-характеристики для предоставления информации о типах]]<br> [[Правило 48: Изучите метапрограммирование шаблонов]]<br> ==== Глава 8 Настройка new и delete ==== [[Правило 49: Разберитесь в поведении обработчика new]]<br> [[Правило 50: Когда имеет смысл заменять new и delete]]<br> [[Правило 51: Придерживайтесь принятых соглашений при написании new и delete]]<br> [[Правило 52: Если вы написали оператор new с размещением, напишите и соответствующий оператор delete]]<br> ==== Глава 9 Разное ==== [[Правило 53: Обращайте внимание на предупреждения компилятора]]<br> [[Правило 54: Ознакомьтесь со стандартной библиотекой, включая TR1]]<br> [[Правило 55: Познакомьтесь с Boost]]<br> b8cb4e75631e360a938d750975a67c61d8369805 Правило 25: Подумайте о поддержке функции swap, не возбуждающей исключений 0 24 53 2013-07-01T05:31:49Z Lerom 3360334 Новая страница: «<P>swap – интересная функция. Изначально она появилась в библиотеке STL и с тех пор стала, во-…» wikitext text/x-wiki <P>swap – интересная функция. Изначально она появилась в библиотеке STL и с тех пор стала, во-первых, основой для написания программ, безопасных в смысле исключений ([[Правило 29: Стремитесь, чтобы программа была безопасна относительно исключений | см. правило 29]]), а во-вторых, общим механизмом решения задачи и присваивания самому себе ([[Правило 11: В operator= осуществляйте проверку на присваивание самому себе | см. правило 11]]). Раз уж swap настолько полезна, то важно реализовать ее правильно, но рука об руку с особой важностью идут и особые сложности. В этом правиле мы исследуем, что они собой представляют и как с ними бороться.</P> <P>Чтобы обменять (swap) значения двух объектов, нужно присвоить каждому из них значение другого. По умолчанию такой обмен осуществляет стандартный алгоритм swap. Его типичная реализация не расходится с вашими ожиданиями:</P> <source lang="cpp"> namespace std { template <typename T> // типичная реализация std::swap void swap(T& a, T& b) // меняет местами значения a и b { T temp(a); a = b; b = temp; } } </source> <P>Коль скоро тип поддерживает копирование (с помощью конструктора копирования и оператора присваивания), реализация swap по умолчанию позволяет объектам этого типа обмениваться значениями без всяких дополнительных усилий с вашей стороны.</P> <P>Стандартная реализация swap, может быть, не приведет вас в восторг. Она включает копирование трех объектов: a в temp, b в a и temp – в b. Для некоторых типов ни одна из этих операция в действительности не является необходимой. Для таких типов swap по умолчанию – быстрый путь на медленную дорожку.</P> <P>Среди таких типов сразу стоит выделить те, что состоят в основном из указателей на другой тип, содержащий реальные данные. Общее название для таких проектных решений: «идиома pimpl» (pointer to implementation – указатель на реализацию – [[Правило 31: Уменьшайте зависимости файлов при компиляции | см. правило 31]]). Спроектированный так класс Widget может быть объявлен следующим образом:</P> <source lang="cpp"> class WidgetImpl { // класс для данных Widget public: // детали несущественны ... private: int a,b,c; // возможно, много данных – std::vector<double> v; // копирование обойдется дорого ... }; class Widget { // класс, использующий идиому pimpl public: Widget(const Widget& rhs); Widget& operator=(const Widget& rhs) // чтоб скопировать Widget, копируем { // его объект WidgetImpl. Детали ... // реализации operator= как такового *pimpl = *(rhs.pimpl); // см. в правилах 10, 11 и 12 ... } ... private: WidgetImpl *pimpl; // указатель на объект с данными }; // этого Widget </source> <P>Чтобы обменять значения двух объектов Widget, нужно лишь обменять значениями их указатели pimpl, но алгоритм swap по умолчанию об этом знать не может. Вместо этого он не только трижды выполнит операцию копирования Widget, но еще и три раза скопирует Widgetlmpl. Очень неэффективно!</P> <P>А нам бы хотелось сообщить функции std::swap, что при обмене объектов Widget нужно обменять значения хранящихся в них указателей pimpl. И такой способ существует: специализировать std::swap для класса Widget. Ниже приведена основная идея, хотя в таком виде код не скомпилируется:</P> <source lang="cpp"> namespace std { template <> // это специализированная версия void swap<Widget>(Widget& a, // std::swap, когда T есть Widget& b) // Widget; не скомпилируется { swap(a.pimpl, b.pimpl); // для обмена двух Widget просто } // обмениваем их указатели pimpl } </source> <P>Строка «template &lt;&gt;» в начале функции говорит о том, что это <EM>полная специализация шаблона</EM> std::swap, а «&lt;Widget&gt;» после имени функции говорит о том, что это специализация для случая, когда T есть Widget. Другими словами, когда общий шаблон swap применяется к Widget, то должна быть использована эта реализация. Вообще-то не допускается изменять содержимое пространства имен std, но разрешено вводить полные специализации стандартных шаблонов (подобных swap) для созданных нами типов (например, Widget). Что мы и делаем.</P> <P>Как я уже сказал, эта функция не скомпилируется. Дело в том, что она пытается получить доступ к указателям pimpl внутри a и b, а они закрыты. Мы можем объявить нашу специализацию другом класса, но соглашение требует поступить иначе: нужно объявить в классе Widget открытую функцию-член по имени swap, которая осуществит реальный обмен значениями, а затем специализация std::swap вызовет эту функцию-член:</P> <source lang="cpp"> class Widget { // все как раньше, за исключением public: // добавления функции-члена swap ... void swap(Widget& other) { using std::swap; // необходимость в этом объявлении // объясняется далее swap(pimpl, other.pimpl); // чтобы обменять значениями два объекта } // Widget,обмениваем указатели pimpl ... }; namespace std { template <> // переделанная версия void swap<Widget>(Widget& a, // std::swap Widget& b) { a.swap(b); // чтобы обменять значениями Widget, } // вызываем функцию-член swap } </source> <P>Этот вариант не только компилируется, но и полностью согласован с STL-контейнерами, каждый из которых предоставляет и открытую функцию-член swap, и специализированную версию std::swap, которая вызывает эту функцию-член.</P> <P>Предположим, однако, что Widget и Widgetlmpl – это не обычные, а <EM>шаблонные</EM> классы. Возможно, это понадобилось для того, чтобы можно было параметризировать тип данных, хранимых в Widgetlmpl:</P> <source lang="cpp"> template <typename T> class WidgetImpl {...}; template <typename T> class Widget {...}; </source> <P>Поместить функцию-член swap в Widget (и при необходимости в Widgetlmpl) в этом случае так же легко, как и раньше, но мы сталкиваемся с проблемой, касающейся специализации std::swap. Вот что мы хотим написать:</P> <source lang="cpp"> namespace std { template <typename T> void swap<Widget<T>>(Widget<T>& a, // ошибка! Недопустимый код Widget<T>& b) { a.swap(b); } } </source> <P>Выглядит совершенно разумно, но все равно неправильно. Мы пытаемся частично специализировать шаблон функции (std::swap), но, хотя C++ допускает частичную специализацию шаблонов класса, он не разрешает этого для шаблонов функций. Этот код не должен компилироваться (если только некоторые компиляторы не пропустят его по ошибке).</P> <P>Когда вам нужно «частично специализировать» шаблон функции, лучше просто добавить перегруженную версию. Примерно так:</P> <source lang="cpp"> namespace std { template <typename T> void swap(Widget<T>& a, // перегрузка std::swap Widget<T>& b) // (отметим отсутствие <...> после { // “swap”), далее объяснено, почему a.swap(b); // этот код некорректен } } </source> <P>Вообще, перегрузка шаблонных функций – нормальное решение, но std – это специальное пространство имен, и правила, которым оно подчиняется, тоже специальные. Можно полностью специализировать шаблоны в std, но нельзя добавлять в std новые шаблоны (или классы, или функции, или что-либо еще). Содержимое std определяется исключительно комитетом по стандартизации C++, и нам запрещено пополнять список того, что они решили включить туда. К сожалению, форма этого запрета может привести вас в смятение. Программы, которые нарушают его, почти всегда компилируются и исполняются, но их поведение не определено! Если вы не хотите, чтобы ваши программы вели себя непредсказуемым образом, то не должны добавлять ничего в std.</P> <P>Что же делать? Нам по-прежнему нужен способ, чтобы разрешить другим людям вызывать swap и иметь более эффективную шаблонную версию. Ответ прост. Мы, как и раньше, объявляем свободную функцию swap, которая вызывает функцию-член swap, но не говорим, что это специализация или перегруженный вариант std::swap. Например, если вся функциональность, касающаяся Widget, находится в пространстве имен WidgetStuff, то это будет выглядеть так:</P> <source lang="cpp"> namespace WidgetStuff { ... // шаблонный WidgetImpl и т. п. template<typename T> // как и раньше, включая class Widget {...}; // функцию-член swap ... template<typename T> // свободная функция swap void swap(Widget<T>& a, // не входит в пространство имен std Widget<T>& b) { a.swap(b); } } </source> <P>Теперь если кто-то вызовет swap для двух объектов Widget, то согласно правилам поиска имен в C++ (а точнее, согласно правилу <EM>учета зависимостей от аргументов)</EM> будет найдена специфичная для Widget версия в пространстве имен WidgetStuff. А это как раз то, что мы хотим.</P> <P>Этот подход работает одинаково хорошо для классов и шаблонов классов, поэтому кажется, что именно его и следует всегда использовать. К сожалению, для классов есть причина, по которой надо специализировать std::swap (я опишу ее ниже), поэтому если вы хотите иметь собственную специфичную для класса версию swap, вызываемую в любых контекстах (а вы, без сомнения, хотите), то придется написать и свободную функцию swap в том же пространстве имен, где находится ваш класс, и специализацию std::swap.</P> <P>Кстати, если вы не пользуетесь пространствам имен, все вышесказанное остается в силе (то есть вам нужна свободная функция swap, которая вызывает функцию-член swap). Но зачем засорять глобальное пространство имен вашими классами, шаблонами, функциями, перечислениями и перечисляемыми константами, определениями типов typedef? Разве вы не имеете понятия о приличиях?</P> <P>Все, что я написал до сих пор, представляет интерес для авторов функции swap, но стоит посмотреть на ситуацию с точки зрения пользователя. Предположим, вы пишете шаблон функции, в котором хотите поменять значениями два объекта:</P> <source lang="cpp"> template <typename T> void doSomething(T& obj1, T& obj2) { ... swap(obj1, obj2); ... } </source> <P>Какая версия swap должна здесь вызываться? Общая – из пространства std, о существовании которой вы точно знаете; ее специализация главного из std, которая может, существует, а может, нет; или специфичная для класса T, существование которой также под вопросом и которая может находиться в каком-то пространстве имен (но заведомо не в std)? Вам хотелось бы вызвать специфичную для T версию, если она существует, а в противном случае к общей версии из std. Вот как удовлетворить это желание:</P> <source lang="cpp"> template <typename T> void doSomething(T& obj1, T& obj2) { using std::swap; // сделать std::swap доступной этой функции ... swap(obj1, obj2); // вызвать лучший вариант swap для объектов типа T ... } </source> <P>Когда компилятор встречает вызов swap, он ищет, какую версию вызвать. Правила разрешения имен в C++ гарантируют, что будет найдена любая специфичная для типа T версия в глобальной области видимости или в том же пространстве имен, что и T. (Например, если T – это Widget в пространстве имен Widget-Stuff, компилятор проанализирует аргументы и найдет именно эту версию.) Если же версии swap, специфичной для T, не существует, то компилятор возьмет swap из std благодаря объявлению using, которая делает std::swap видимой. Но даже в этом случае компилятор предпочтет специализацию std::swap для типа T общему шаблону.</P> <P>Таким образом, заставить компилятор вызвать нужную вам версию swap достаточно просто. Единственное, о чем следует позаботиться, – не квалифицировать вызов именем пространства имен, потому что это влияет на способ выбора функции. Например, если вы напишете вызов следующим образом:</P> <source lang="cpp"> std::swap(obj1, obj2); // неправильный способ вызова swap </source> <P>то заставите компилятор рассматривать только swap из пространства std (включая все специализации шаблонов), исключив возможность отыскания более подходящей версии, специфичной для типа T, даже если она где-то определена. К сожалению, некоторые программисты по ошибке квалифицируют вызов swap таким образом, поэтому важно предоставлять в своем классе полную специализацию std::swap, тогда даже в таком, неправильно написанном коде специфичная для типа реализация swap окажется доступной. (Подобный код присутствует в некоторых реализациях стандартной библиотеки, поэтому в ваших интересах – сделать все, чтобы он работал эффективно).</P> <P>Итак, мы обсудили реализацию swap по умолчанию, в виде функции-члена класса, в виде свободной функции и в виде специализации std::swap, а также вызовы swap. Теперь подведем итоги.</P> <P>Во-первых, если реализация swap по умолчанию обеспечивает приемлемую эффективность для ваших классов или шаблонов классов, то вам не нужно делать ничего. Всякий, кто попытается обменять значения объектов вашего класса, получит версию по умолчанию, и она будет прекрасно работать.</P> <P>Во-вторых, если реализация по умолчанию swap недостаточно эффективна (что почти всегда означает, что ваш класс или шаблон использует некоторую вариацию идиомы pimpl), сделайте следующее:</P> <P>1) предоставьте открытую функцию-член, которая эффективно обменивает значения двух объектов вашего типа. По причинам, которые я сейчас объясню, эта функция никогда не должна возбуждать исключений;</P> <P>2) предоставьте свободную функцию swap в том же пространстве имен, что и ваш класс или шаблон. Пусть она вызывает вашу функцию-член;</P> <P>3) если вы пишете класс (а не шаблон), специализируйте std::swap для вашего класса. Пусть она также вызывает вашу функцию-член.</P> <P>Наконец, если вы вызываете swap, убедитесь, что включено using-объявление, которое вводит std::swap в область видимости вашей функции, а затем вызывайте swap без квалификации пространства имен.</P> <P>Я еще забыл предупредить, что версия функции-члена swap никогда не должна возбуждать исключений. Дело в том, что одно из наиболее частых применений swap – помочь классам (и шаблонам классов) в предоставлении надежных гарантий безопасности исключений. В [[Правило 29: Стремитесь, чтобы программа была безопасна относительно исключений | правиле 29]] вы найдете подробную информацию на эту тему, а сейчас лишь подчеркнем, что в основе этого приема лежит предположение о том, что swap, реализованная в виде функции-члена, никогда не возбуждает исключений. Это ограничение касается только функции-члена! Оно не относится к реализации swap в виде свободной функции, поскольку стандартная версия swap по умолчанию основана на конструкторах копирования и операторе присваивания, а этим функциям разрешено возбуждать исключения. Когда вы пишете собственную версию swap, то обычно представляете не просто эффективный способ обмена значений, а такой, при котором не возбуждаются исключения. Общее правил таково: эти две характеристики swap идут рука об руку, потому что высокоэффективные операции обмена всегда основаны на операциях над встроенными типами (такими как указатели, лежащие в основе идиомы pimpl), а операции над встроенными типами никогда не возбуждают исключений.</P> == Что следует помнить == *Предоставьте функцию-член swap, если std::swap работает с вашим типом неэффективно. Убедитесь, что она не возбуждает исключений. *Если вы предоставляете функцию-член swap, то также предоставьте свободную функцию, вызывающую функцию-член. Для классов (не шаблонов) специализируйте также std::swap. *Когда вызывается swap, используйте using-объявление, вводящее std::swap в область видимости, и вызывайте swap без квалификатора пространства имен. *Допускается предоставление полной специализации шаблонов, находящихся в пространстве имен std, для пользовательских типов, но никогда не пытайтесь добавить в пространство std что-либо новое. b4b17473f6bb8ece44c54b1281dcec10b2ffba41 Правило 26: Откладывайте определение переменных насколько возможно 0 25 54 2013-07-01T05:48:52Z Lerom 3360334 Новая страница: «<P>Всякий раз при объявлении переменной, принадлежащий типу, в котором есть конструктор и…» wikitext text/x-wiki <P>Всякий раз при объявлении переменной, принадлежащий типу, в котором есть конструктор или деструктор, программа тратит время на ее конструирование, когда поток управления достигнет определения переменной, и на уничтожение – при выходе переменной из области видимости. Эти накладные расходы приходится нести даже тогда, когда переменная не используется, и, разумеется, их хотелось бы избежать.</P> <P>Вероятно, вы думаете, что никогда не объявляете неиспользуемых переменных, но так ли это? Рассмотрим следующую функцию, которая возвращает зашифрованный пароль при условии, что его длина не меньше некоторого минимума. Если пароль слишком короткий, функция возбуждает исключение типа logic_error, определенное в стандартной библиотеке C++ ([[Правило 54: Ознакомьтесь со стандартной библиотекой, включая TR1 : см. правило 54]]):</P> <source lang="cpp"> // эта функция объявляет переменную encrypted слишком рано std::string encryptPassword(const std::string& password) { using namespace std; string encrypted; if(password.length() < MinimumPasswordLength) { throw logic_error(“Слишком короткий пароль”); } ... // сделать все, что необходимо для помещения // зашифрованного пароля в переменную encrypted return encrypted; } </source> <P>Нельзя сказать, что объект encrypted в этой функции совсем уж не используется, но он не используется в случае, когда возбуждается исключение. Другими словами, вы платите за вызов конструктора и деструктора объекта encrypted, даже если функция encryptPassword возбуждает исключение. Так не лучше ли отложить определение переменной encrypted до того момента, когда вы будете <EM>знать,</EM> что она нужна?</P> <source lang="cpp"> // в этой функции определение переменной encrypted отложено до момента, // когда в ней возникает надобность std::string encryptPassword(const std::string& password) { using namespace std; if(password.length() < MinimumPasswordLength) { throw logic_error(“Слишком короткий пароль”); } string encrypted; ... // сделать все, что необходимо для помещения // зашифрованного пароля в переменную encrypted return encrypted; } </source> <P>Этот код все еще не настолько компактный, как мог бы быть, потому что переменная encrypted определена без начального значения. А значит, будет использован ее конструктор по умолчанию. Часто первое, что нужно сделать с объектом, – это дать ему какое-то значение, нередко посредством присваивания. В [[Правило 4: Прежде чем использовать объекты, убедитесь, что они инициализированы | правиле 4]] объяснено, почему конструирование объектов по умолчанию с последующим присваиванием значения менее эффективно, чем инициализация нужным значением с самого начала. Это относится и к данному случаю. Например, предположим, что для выполнения «трудной» части работы функция encryptPassword вызывает следующую функцию:</P> <source lang="cpp"> void encrypt(std::string& s); // шифрует s по месту </source> <P>Тогда encryptPassword может быть реализована следующим образом, хотя и это еще не оптимальный способ:</P> <source lang="cpp"> // в этой функции определение переменной encrypted отложено до момента, // когда в ней возникает надобность, но и этот вариант еще недостаточно эффективен std::string encryptPassword(const std::string& password) { ... // проверка длины string encrypted; // конструктор по умолчанию encrypted = password; // присваивание encrypted encrypt(encrypted); return encrypted; } </source> <P>Еще лучше инициализировать encrypted параметром password, избежав таким образом потенциально дорогостоящего конструктора по умолчанию:</P> <source lang="cpp"> // а это оптимальный способ определения и инициализации encrypted std::string encryptPassword(const std::string& password) { ... // проверка длины string encrypted(password); // определение и инициализация // конструктором копирования encrypt(encrypted); return encrypted; } </source> <P>Это и означает «откладывать насколько возможно» (как сказано в заголовке правила). Вы не только должны откладывать определение переменной до того момента, когда она используется, нужно еще постараться отложить определение до получения аргументов для инициализации. Поступив так, вы избегаете конструирования и разрушения ненужных объектов, а также излишних вызовов конструкторов по умолчанию. Более того, это помогает документировать назначение переменных за счет инициализации их в том контексте, в котором их значение понятно без слов.</P> <P>«А как насчет циклов?» – можете удивиться вы. Если переменная используется только внутри цикла, то что лучше: определить ее вне цикла и выполнять присваивание на каждой итерации или определить ее внутри цикла? Другими словами, какая из следующих конструкций предпочтительнее?</P> <source lang="cpp"> // Подход A: определение вне цикла Widget w; for(int i=0; i<n; ++i) { w = некоторое значение, зависящее от i; ... } // Подход B: определение внутри цикла for(int i=0; i<n; ++i) { Widget w(некоторое значение, зависящее от i); ... } </source> <P>Здесь я перехожу от объекта типа string к объекту типа Widget, чтобы избежать любых предположений относительно стоимости конструирования, разрушения и присваивания.</P> <P>В терминах операций Widget накладные расходы вычисляются так:</P> *Подход A: 1 конструктор + 1 деструктор + n присваиваний *Подход B: n конструкторов + n деструкторов <P>Для классов, в которых стоимость операции присваивания меньше, чем пары конструктор-деструктор, подход A обычно более эффективен. Особенно это верно, когда значение n достаточно велико. В противном случае, возможно, подход B лучше. Более того, в случае A имя w видимо в более широкой области (включающей в себя цикл), чем в случае B, а иногда это делает программу менее понятной и удобной для сопровождения. Поэтому если (1) нет априорной информации о том, что присваивание обходится дешевле, чем пара конструктор-деструктор, и (2) речь идет о части программы, производительность которой критична, то по умолчанию рекомендуется использовать подход B.</P> == Что следует помнить == *Откладывайте определение переменных насколько возможно. Это делает программы яснее и повышает их эффективность. ad6a1ce390af40d618db18f74816fffa4432371f Правило 27: Не злоупотребляйте приведением типов 0 26 55 2013-07-08T12:32:50Z Lerom 3360334 Новая страница: «<P>Правила C++ разработаны так, чтобы неправильно работать с типами было невозможно. Теоре…» wikitext text/x-wiki <P>Правила C++ разработаны так, чтобы неправильно работать с типами было невозможно. Теоретически, если ваша программа компилируется без ошибок, значит, она не пытается выполнить никаких небезопасных или бессмысленных операций с объектами. Это ценная гарантия. Не надо от нее отказываться.</P> <P>К сожалению, приведения обходят систему типов. И это может привести к различным проблемам, некоторые из которых распознать легко, а некоторые – чрезвычайно трудно. Если вы пришли к C++ из мира C, Java или C#, примите эток сведению, поскольку в указанных языках в приведениях типов чаще возникает необходимость, и они менее опасны, чем в C++. Но C++ – это не C. Это не Java. Это не C#. В этом языке приведение – это средство, к которому нужно относиться с должным почтением.</P> <P>Начнем с обзора синтаксиса операторов приведения типов, потому что существует три разных способа написать одно и то же. Приведение в стиле C выглядит так:</P> <source lang="cpp"> (T) <EM>expression</EM> // привести <EM>expression</EM> к типу T </source> <P>Функциональный синтаксис приведения таков:</P> <source lang="cpp"> T( <EM>expression)</EM> // привести <EM>expression</EM> к типу T </source> <P>Между этими двумя формами нет ощутимого различия, просто скобки расставляются по-разному. Я называю эти формы <EM>приведениями в старом стиле.</EM></P> <P>C++ также представляет четыре новые формы приведения типов (часто называемые приведениями <EM>в стиле С++):</EM></P> <source lang="cpp"> const_cast<T>(expression) dynamic_cast<T>(expression) reinterpret_cast<T>(expression) static_cast<T>(expression) </source> <P>У каждой из них свое назначение:</P> *const_cast обычно применяется для того, чтобы отбросить константность объекта. Никакое другое приведение в стиле C++ не позволяет это сделать; *dynamic_cast применяется главным образом для выполнения «безопасного понижающего приведения» (downcasting). Этот оператор позволяет определить, принадлежит ли объект данного типа некоторой иерархии наследования. Это единственный вид приведения, который не может быть выполнен с использованием старого синтаксиса. Это также единственное приведение, которое может потребовать ощутимых затрат во время исполнения (подробнее позже); *reinterpret_cast предназначен для низкоуровневых приведений, которые порождают зависимые от реализации (то есть непереносимые) результаты, например приведение указателя к int. Вне низкоуровневого кода такое приведение должно использоваться редко. Я использовал его в этой книге лишь однажды, когда обсуждал написание отладочного распределителя памяти ([[Правило 50: Когда имеет смысл заменять new и delete | см. правило 50]]); *static_cast может быть использован для явного преобразования типов (например, неконстантных объектов к константным (как в [[Правило 3: Везде, где только можно используйте const | правиле 3]]), int к double и т. п.). Он также может быть использован для выполнения обратных преобразований (например, указателей void* к типизированным указателям, указателей на базовый класс к указателю на производный). Но привести константный объект к неконстантному этот оператор не может (это вотчина const_cast). <P>Применение приведений в старом стиле остается вполне законным, но новые формы предпочтительнее. Во-первых, их гораздо легче найти в коде (и для человека, и для инструмента, подобного grep), что упрощает процесс поиска в коде тех мест, где система типизации подвергается опасности. Во-вторых, более узко специализированное назначение каждого оператора приведения дает возможность компиляторам диагностировать ошибки их использования. Например, если вы попытаетесь избавиться от константности, используя любой оператор приведения в стиле C++, кроме const_cast, то ваш код не откомпилируется.</P> <P>Я использую приведение в старом стиле только тогда, когда хочу вызвать explicit конструктор, чтобы передать объект в качестве параметра функции. Например:</P> <source lang="cpp"> class Widget { public: explicit Widget(int size); ... }; void doSomeWork(const Widget& w); doSomeWork(Widget(15)); // создать Widget из int // с функциональным приведением doSomeWork(static_cast<Widget>(15)); // создать Widget из int // с приведением в стиле C++ </source> <P>Но намеренное создание объекта не «ощущается» как приведение типа, поэтому в данном случае, наверное, лучше применить функциональное приведение вместо static_cast. Да и вообще, код, ведущий к аварийному завершению, обычно выглядит совершенно разумным, когда вы его пишете, поэтому лучше не обращать внимания на ощущения и всегда пользоваться приведениями в новом стиле.</P> <P>Многие программисты полагают, что приведение типа всего лишь говорит компилятору, что нужно трактовать один тип как другой, но они заблуждаются. Преобразования типа любого рода (как явные, посредством приведения, так и неявные, выполняемые самим компилятором) часто приводят к появлению кода, исполняемого во время работы программы. Рассмотрим пример:</P> <source lang="cpp"> int x, y; ... double d = static_cast<double>(x)/y; // деление x на y с использованием // деления с плавающей точкой </source> <P>Приведение int x к типу double почти наверняка порождает исполняемый код, потому что в большинстве архитектур внутреннее представление int отличается от представления double. Если это вас не особенно удивило, но взгляните на следующий пример:</P> <source lang="cpp"> class Base {...}; class Derived: public Base {...}; Derived d; Base *pb = &d; // неявное преобразование Derived* // в Base* </source> <P>Здесь мы всего лишь создали указатель базового класса на объект производного, но <STRONG>иногда</STRONG> эти два указателя указывают вовсе не на одно и то же. В таком случае <EM>во время исполнения</EM> к указателю Derived* прибавляется смещение, чтобы получить правильное значение указателя Base*.</P> <P>Последний пример демонстрирует, что один и тот же объект (например, объект типа Derived) может иметь более одного адреса (например, адрес при указании на него как на Base* отличается от адреса при указании как на Derived*). Такое невозможно в C. Такое невозможно в Java. Такого не бывает в C#. Но это случается в C++. Фактически, когда применяется множественное наследование, такое случается сплошь и рядом, но может произойти и при одиночном наследовании. Это ко всему прочему означает, что, программируя на C++, вы не должны строить предположений о том, как объекты располагаются в памяти, и уж тем более не должны выполнять приведение типов на базе этих предположений. Например, приведение адреса объекта к типу char* и последующее использование арифметических операций над указателями почти всегда становятся причиной неопределенного поведения.</P> <P>Заметьте, я сказал, что смещение требуется прибавлять «иногда». Способы размещения объектов в памяти и способы вычисления их адресов изменяются от компилятора к компилятору. А значит, из того, что «вы знаете, как хранится объект в памяти» на одной платформе, вовсе не следует, что на других все будет устроено точно так же. Мир полон программистов, которые усвоили этот урок, заплатив слишком высокую цену.</P> <P>Интересный момент, касающийся приведений, – еще в том, что легко написать код, который выглядит правильным (и может быть правильным на других языках), но на самом деле правильным не является. Например, во многих каркасах для разработки приложений требуется, чтобы виртуальные функции-члены, определенные в производных классах, вначале вызывали соответствующие функции из базовых классов. Предположим, что у нас есть базовый класс Window и производный от него класс SpecialWindow, причем в обоих определена виртуальная функция onResize. Далее предположим, что onResize из SpecialWindow будет вызывать сначала onResize из Window. Следующая реализация выглядит хорошо, но по сути неправильна:</P> <source lang="cpp"> class Window { // базовый класс public: virtual void onResize() {...} // реализация onResize в базовом ... // классе }; class SpecialWindow: public Window { // производный класс public: virtual void onResize() { // реализация onResize static_cast<Window>(*this).onResize(); // в производном классе; // приведение *this к Window, // затем вызов его onResize; // это не работает! ... // выполнение специфической для } // SpecialWindow части onResize ... }; </source> <P>Я выделил в этом коде приведение типа. (Это приведение в новом стиле, но использование старого стиля ничего не меняет.) Как и ожидается, *this приводит к типу Window. Поэтому обращение к onResize приводит к вызову Window::onResize. Вот только эта функция не будет вызвана для текущего объекта! Неожиданно, не правда ли? Вместо этого оператор приведения создаст новую, временную копию части базового класса *this и вызовет onResize для этой копии! Приведенный выше код не вызовет Window::onResize для текущего объекта с последующим выполнением специфичных для SpecialWindow действий – он выполнит Window::onResize для <EM>копии части базового класса</EM> текущего объекта перед выполнением специфичных для SpecialWindow действий для данного объекта. Если Window::onResize модифицирует объект (что вполне возможно, так как onResize – не константная функция-член), то текущий объект не будет модифицирован. Вместо этого будет модифицирована <EM>копия</EM> этого объекта. Однако если SpecialWindow::onResize модифицирует объект, то будет модифицирован именно текущий объект. И в результате текущий объект остается в несогласованном состоянии, потому что модификация той его части, что принадлежит базовому классу, не будет выполнена, а модификация части, принадлежащей производному классу, будет.</P> <P>Решение проблемы в том, чтобы исключить приведение типа, заменив его тем, что вы действительно имели в виду. Нет необходимости выполнять какие-то трюки с компилятором, заставляя его интерпретировать *this как объект базового класса. Вы хотите вызвать версию onResize базового класса для текущего объекта. Так поступите следующим образом:</P> <source lang="cpp"> class SpecialWindow: public Window { public: virtual void onResize() { Window::onResize(); // вызов Window::onResize на *this ... } ... }; </source> <P>Приведенный пример также демонстрирует, что коль скоро вы ощущаете желание выполнить приведение типа, это знак того, что вы, возможно, на ложном пути. Особенно это касается оператора dynamic_cast.</P> <P>Прежде чем вдаваться в детали dynamic_cast, стоит отметить, что большинство реализаций этого оператора работают довольно медленно. Так, по крайней мере, одна из распространенных реализаций основана на сравнении имен классов, представленных строками. Если вы выполняете dynamic_cast для объекта класса, принадлежащего иерархии с одиночным наследованием глубиной в четыре уровня, то каждое обращение к dynamic_cast в такой реализации может обойтись вам в четыре вызова strcmp для сравнения имен классов. Для более глубокой иерархии или такой, в которой имеется множественное наследование, эта операция окажется еще более дорогостоящей. Есть причины, из-за которых некоторые реализации работают подобным образом (потому что они должны поддерживать динамическую компоновку). Таким образом, в дополнение к настороженности по отношению к приведениям типов в принципе вы должны проявлять особый скептицизм, когда речь идет о применении dynamic_cast в части программы, для которой производительность стоит на первом месте.</P> <P>Необходимость в dynamic_cast обычно появляется из-за того, что вы хотите выполнить операции, определенные в производном классе, для объекта, который, как вы полагаете, принадлежит производному классу, но при этом у вас есть только указатель или ссылка на базовый класс, посредством которой нужно манипулировать объектом. Есть два основных способа избежать этой проблемы.</P> <P>Первый – используйте контейнеры для хранения указателей (часто «интеллектуальных», [[Правило 13: Используйте объекты для управления ресурсами | см. правило 13]]) на сами объекты производных классов, тогда отпадет необходимость манипулировать этими объектами через интерфейсы базового класса. Например, если в нашей иерархии Window/SpecialWindow только SpecialWindow поддерживает мерцание (blinking), то вместо:</P> <source lang="cpp"> class Window { ...}; class SpecialWindow { public: void blink(); ... }; typedef // см. правило 13 std::vector<std::tr1::shared_ptr<Window>>VPW; // о tr1::shared_ptr VPW winPtrs; ... for (VPW::iterator iter = winPtrs.begin(); // нежелательный код: iter!=winPtrs.end(); // применяется dynamic_cast ++iter){ if(SpecialWindow psw = dynamic_cast<SpecialWindow>(iter->get())) psw->blink(); } </source> <P>попробуйте сделать так:</P> <source lang="cpp"> typedef std::vector<std::tr1::shared_ptr<SpecialWindow>> VPSW; VPSW winPtrs; ... for (VPSW::iterator iter = winPtrs.begin(); // это лучше: iter != winPtrs.end(); // не использует dynamic_cast ++iter) (*iter)->blink(); </source> <P>Конечно, такой подход не позволит вам хранить указатели на объекты всех возможных производных от Window классов в одном и том же контейнере. Чтобы работать с разными типами окон и обеспечить безопасность по отношению к типам, вам может понадобиться несколько контейнеров.</P> <P>Альтернатива, которая позволит манипулировать объектами всех возможных производных от Window классов через интерфейс базового класса, – это предусмотреть виртуальные функции в базовом классе, которые позволят вам делать именно то, что вам нужно. Например, хотя только SpecialWindow умеет мерцать, может быть, имеет смысл объявить функцию в базовом классе и обеспечить там реализацию по умолчанию, которая не делает ничего:</P> <source lang="cpp"> class Window { public: virtual void blink() {} // реализация по умолчанию – пустая ... // операция, см. в правиле 34 – почему }; // наличие реализации по умолчанию // может оказаться неудачной идеей class SpecialWindow: public Window { public: virtual void blink() {...} ... }; typedef std::vector<std::tr1::shared_ptr<Window>>VPW; VPW winPtrs; // контейнер содержит // (указатели на) все возможные ... // типы окон for(VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter) // dynamic_cast не используется (*iter)->blink(); </source> <P>Ни один из этих подходов – с применением безопасных по отношению к типам контейнеров или перемещением виртуальной функции вверх по иерархии – не является универсально применимым, но во многих случаях они представляют полезную альтернативу dynamic_cast. Пользуйтесь ими, когда возможно.</P> <P>Но вот чего стоит избегать всегда – это каскадов из операторов dynamic_cast, то есть чего-то вроде такого кода:</P> <source lang="cpp"> class Window {...}; ... // здесь определены производные классы typedef std::vector<std::tr1::shared_ptr<Window>> VPW; VPW winPtrs; ... for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter) { if (SpecialWindow1 *psw1= dynamic_cast<SpecialWindow1>(iter->get())) {...} else if (SpecialWindow2 *psw2= dynamic_cast<SpecialWindow2>(iter->get())) {...} else if (SpecialWindow2 *psw2= dynamic_cast<SpecialWindow2>(iter->get())) {...} ... } </source> <P>В этом случае генерируется объемный и медленный код, к тому же он нестабилен, потому что при каждом изменении иерархии классов Window весь этот код нужно пересмотреть на предмет обновления. Например, если добавится новый производный класс, то вероятно, придется добавить еще одну ветвь в предложение if. Подобный код почти всегда должен быть заменен чем-то на основе вызова виртуальных функций.</P> <P>В хорошей программе на C++ приведения типов используются очень редко, но полностью отказываться от них тоже не стоит. Так, показанное выше приведение int к double является разумным, хотя и не абсолютно необходимым (код может быть переписан с объявлением новой переменной типа double, инициируемой значением x). Как и большинство сомнительных конструкций, приведения типов должны быть изолированы насколько возможно. Обычно они помещаются внутрь функций, чей интерфейс скрывает от пользователей те некрасивые дела, что творятся внутри.</P> == Что следует помнить == *Избегайте насколько возможно приведений типов, особенно dynamic_cast, в критичном по производительности коде. Если дизайн требует приведения, попытайтесь разработать альтернативу, где такой необходимости не возникает. *Когда приведение типа необходимо, постарайтесь скрыть его внутри функции. Тогда пользователи смогут вызывать эту функцию вместо помещения приведения в их собственный код. *Предпочитайте приведения в стиле C++ старому стилю. Их легче увидеть, и они более избирательны. 71beebf2bcfe84e23d7dd3575feaa8622005d3e1 Правило 28: Избегайте возвращения «дескрипторов» внутренних данных 0 27 56 2013-07-08T12:53:16Z Lerom 3360334 Новая страница: «<P>Представим, что вы работаете над приложением, имеющим дело с прямоугольниками. Каждый …» wikitext text/x-wiki <P>Представим, что вы работаете над приложением, имеющим дело с прямоугольниками. Каждый прямоугольник может быть представлен своим левым верхним углом и правым нижним. Чтобы объект Rectangle оставался компактным, вы можете решить, что описание определяющих его точек следует вынести из Rectangle во вспомогательную структуру:</P> <source lang="cpp"> class Point { // класс, представляющий точки public: Point(int x, int y); ... void setX(int newVal); void setY(int newVal); ... }; struct RectData { // точки, определяющие Rectangle Point ulhc; // ulhc – верхний левый угол Point lrhc; // lrhc – нижний правый угол }; class Rectangle { ... private: std::tr1::shared_ptr<RectData> pData; // см. в правиле 13 }; // информацию о tr1::shared_ptr </source> <P>Поскольку пользователям класса Rectangle понадобится определять его координаты, то класс предоставляет функции upperLeft и lowerRight. Однако Point – это определенный пользователем тип, поэтому, помня о том, что передача таких типов по ссылке обычно более эффективна, чем передача по значению ([[Правило 20: Предпочитайте передачу по ссылке на const передаче по значению | см. правило 20]]), эти функции возвращают ссылки на внутренние объекты Point:</P> <source lang="cpp"> class Rectangle { public: ... Point& upperLeft() const { return pData->ulhc;} Point& lowerRight() const { return pData->lrhc;} ... }; </source> <P>Такой вариант откомпилируется, но он неправильный! Фактически он внутренне противоречив. С одной стороны, upperLeft и lowerRight объявлены как константные функции-члены, поскольку они предназначены только для того, чтобы предоставить клиенту способ получить информацию о точках Rectangle, не давая ему возможности модифицировать объект Rectangle ([[Правило 3: Везде, где только можно используйте const | см. правило 3]]). С другой стороны, обе функции возвращают ссылки на закрытые внутренние данные – ссылки, которые пользователь может затем использовать для модификации этих внутренних данных! Например:</P> <source lang="cpp"> Point coord1(0, 0); Point coord2(100,100); const Rectangle rec(coord1, coord2); // rec – константный прямоугольник // от (0, 0) до (100, 100) rec.upperLeft().setX(50); // теперь rec лежит между // (50, 0) и (100, 100)! </source> <P>Обратите внимание, что пользователь функции upperLeft может использовать возвращенную ссылку на один из данных-членов внутреннего объекта Point для модификации этого члена. Но ведь ожидается, что rec – константа!</P> <P>Из этого примера следует извлечь два урока. Первый – член данных инкапсулирован лишь настолько, насколько доступна функция, возвращающая ссылку на него. В данном случае хотя ulhc и lrhc объявлены закрытыми, но на самом деле они открыты, потому что на них возвращают ссылки открытые функции upperLeft и lowerRight. Второй урок в том, что если константная функция-член возвращает ссылку на данные, ассоциированные с объектом, но хранящиеся вне самого объекта, то код, вызывающий эту функцию, может модифицировать данные. (Все это последствия ограничений побитовой константности – [[Правило 3: Везде, где только можно используйте const | см. правило 3]].)</P> <P>Такой результат получился, когда мы использовали функции-члены, возвращающие ссылки, но если они возвращают указатели или итераторы, проблема остается, и причины те же. Ссылки, указатели и итераторы – все это «дескрипторы» (handles), и возвращение такого «дескриптора» внутренних данных объекта – прямой путь к нарушению принципов инкапсуляции. Как мы только что видели, это может привести к тому, что константные функции-члены позволят модифицировать состояние объекта.</P> <P>Обычно, говоря о внутреннем устройстве объекта, мы имеем в виду его данные-члены, но функции-члены, к которым нет открытого доступа (то есть объявленные в секции private или protected), также являются частью внутреннего устройства. Поэтому возвращать их «дескрипторы» тоже не следует. Иными словами, нельзя, чтобы функция-член возвращала указатель на менее доступную функцию-член. В противном случае реальный уровень доступа будет определять более доступная функция, потому что клиенты смогут получить указатель на менее доступную функцию и вызвать ее через такой указатель.</P> <P>Впрочем, функции, которые возвращают указатели на функции-члены, встречаются нечасто, поэтому вернемся к классу Rectangle и его функциям-членам upperLeft и lowerRight. Обе проблемы, которые мы идентифицировали для этих функций, могут быть исключены простым применением квалификатора const к их возвращаемому типу:</P> <source lang="cpp"> class Rectangle { public: ... const Point& upperLeft() const { return pData->ulhc;} const Point& lowerRight() const { return pData->lrhc;} ... }; </source> <P>В результате такого изменения пользователи смогут читать объекты Point, определяющие прямоугольник, но не смогут изменять их. Это значит, что объявление константными функций upperLeft и lowerRight больше не является ложью, так как они более не позволяют клиентам модифицировать состояние объекта. Что касается проблемы инкапсуляции, то мы с самого начала намеревались дать клиентам возможность видеть объекты Point, определяющие Rectangle, поэтому в данном случае ослабление инкапсуляции намеренное. К тому же это лишь <EM>частичное</EM> ослабление: рассматриваемые функции дают только доступ для чтения. Доступ для записи по-прежнему запрещен.</P> <P>Но даже и так upperLeft и lowerRight по-прежнему возвращают «дескрипторы» внутренних данных объекта, и это может вызвать проблемы иного свойства. В частности, возможно появление «висячих дескрипторов» (dangling handles), то есть дескрипторов, ссылающихся на части уже не существующих объектов. Наиболее типичный источник таких исчезнувших объектов – значения, возвращаемые функциями. Например, рассмотрим функцию, которая возвращает ограничивающий прямоугольник объекта GUI:</P> <source lang="cpp"> class GUIObject {...}; const Rectangle // возвращает прямоугольник по значению; boundBox(const GUIObject& obj); // см. в правиле 3, почему const </source> <P>Теперь посмотрим, как пользователь может применить эту функцию:</P> <source lang="cpp"> GUIObject *pgo; // pgo указывает на некий объект ... // GUIObject const Point *pUpperLeft = // получить указатель на верхний левый &(boundingBox(*pgo).upperLeft()); // угол его рамки </source> <P>Вызов boundingBox вернет новый временный объект Rectangle. Этот объект не имеет имени, поэтому назовем его <EM>temp.</EM> Затем вызывается функция-член <EM>upperLeft</EM> объекта <EM>temp,</EM> и этот вызов возвращает ссылку на внутренние данные <EM>temp,</EM> в данном случае на один из объектов Point. В результате pUpperLeft указывает на этот объект Point. До сих пор все шло хорошо, но мы еще не закончили, поскольку в конце предложения возвращенное boundingBox значение – <EM>temp</EM> – будет разрушено, а это приведет к разрушению объектов Point, принадлежавших <EM>temp.</EM> То есть pUpperLeft теперь указывает на объект, который более не существует. Указатель PUpperLeft становится «висячим» уже в конце предложения, где он создан!</P> <P>Вот почему опасна любая функция, которая возвращает «дескриптор» внутренних данных объекта. При этом не важно, является ли «дескриптор» ссылкой, указателем или итератором. Не важно, что она квалифицирована const. Не важно, что сама функция-член, возвращающая «дескриптор», является константной. Имеет значение лишь тот факт, что «дескриптор» возвращен, поскольку возникает опасность, что он «переживет» объект, с которым связан.</P> <P>Это не значит, что <EM>никогда</EM> не следует писать функции-члены, возвращающие дескрипторы. Иногда это бывает необходимо. Например, operator[] позволяет вам обращаться к отдельному элементу строки или вектора, и работает он, возвращая ссылку на данные в контейнере ([[Правило 3: Везде, где только можно используйте const | см. правило 3]]), которые уничтожаются вместе с контейнером. Но все же такие функции – скорее исключение, чем правило.</P> == Что следует помнить == *Избегайте возвращать «дескрипторы» (ссылки, указатели, итераторы) внутренних данных объекта. Это повышает степень инкапсуляции, помогает константным функциям-членам быть константными и минимизирует вероятность появления «висячих дескрипторов». aaae49987731beb2d6fcbf5c2cf18fe8919e0614 Правило 29: Стремитесь, чтобы программа была безопасна относительно исключений 0 28 58 2013-07-11T09:01:39Z Lerom 3360334 Новая страница: «<P>Безопасность исключений в чем-то подобна беременности… но пока отложим эту мысль в ст…» wikitext text/x-wiki <P>Безопасность исключений в чем-то подобна беременности… но пока отложим эту мысль в сторонку. Нельзя всерьез говорить о репродуктивной функции, пока не завершился этап ухаживания.</P> <P>Предположим, что у нас есть класс, представляющий меню с фоновыми картинками в графическом интерфейсе пользователя. Этот класс предназначен для использования в многопоточной среде, поэтому он включает мьютекс для синхронизации доступа:</P> <source lang="cpp"> class PrettyMenu { public: ... void changeBackground(std::istream& imgSrc); // сменить фоновую ... // картинку private: Mutex mutex; // мьютекс объекта Image *bgImage; // текущая фоновая картинка int imageChanges; // сколько раз картинка менялась }; </source> <P>Рассмотрим следующую возможную реализацию функции-члена change-Background:</P> <source lang="cpp"> void PrettyMenu::changeBackground(std::istreamS lmgSrc) { lock(Smutex); // захватить мьютекс delete bglmage; // избавиться от старой картинки ++imageChanges; // обновить счетчик изменений картинки bglmage = new Image(lmgSrc); // установить новый фон unlock(Smutex); // освободить мьютекс } </source> <P>С точки зрения безопасности исключений, эта функция настолько плоха, насколько вообще возможно. К безопасности исключений предъявляется два требования, и она не удовлетворяет ни одному из них.</P> <P>Когда возбуждается исключение, то безопасная относительно исключений функция:</P> *<STRONG>Не допускает утечки ресурсов.</STRONG> Приведенный код не проходит этот тест, потому что если выражение «new Image(imgSrc)» возбудит исключение, то вызов unlock никогда не выполнится, и мьютекс окажется захваченным навсегда. *<STRONG>Не допускает повреждения структур данных.</STRONG> Если «new Image(imgSrc)» возбудит исключение, в bgImage останется указатель на удаленный объект. Кроме того, счетчик imageChanges увеличивается, несмотря на то что новая картинка не установлена. (С другой стороны, старая картинка уже полностью удалена, так что трудно сделать вид, будто ничего не изменилось.) <P>Справиться с утечкой ресурсов легко – в [[Правило 13: Используйте объекты для управления ресурсами | правиле 13]] объяснено, как пользоваться объектами, управляющими ресурсами, а в [[Правило 14: Тщательно продумывайте поведение при копировании классов, управляющих ресурсами | правиле 14]] представлен класс Lock, гарантирующий своеременное освобождение мьютексов:</P> <source lang="cpp"> void PrettyMenu::changeBackground(std::istreamS lmgSrc) { Lock ml(mutex); // из правила 14: захватить мьютекс // и гарантировать его последующее освобождение delete bglmage; ++imageChanges; bglmage = new Image(lmgSrc); } </source> <P>Одним из преимуществ классов для управления ресурсами, подобных Lock, является то, что обычно они уменьшают размер функций. Заметили, что вызов unlock уже не нужен? Общее правило гласит: чем меньше кода, тем лучше, потому что меньше возможностей для ошибок и меньше путаницы при внесении изменений.</P> <P>От утечки ресурсов перейдем к проблеме возможного повреждения данных. Здесь у нас есть выбор, но прежде чем его сделать, нужно уточнить терминологию.</P> <P>Безопасные относительно исключений функции предоставляют одну из трех гарантий.</P> *Функции, предоставляющие <STRONG>базовую гарантию,</STRONG> обещают, что если исключение будет возбуждено, то все в программе остается в корректном состоянии. Никакие объекты или структуры данных не повреждены, и все объекты находятся в непротиворечивом состоянии (например, все инварианты классов не нарушены). Однако точное состояние программы может быть непредсказуемо. Например, мы можем написать функцию change-Background так, что при возникновении исключения объект PrettyMenu сохранит старую фоновую картинку либо у него будет какой-то фон по умолчанию, но пользователи не могут заранее знать, какой. (Чтобы выяснить это, им придется вызвать какую-то функцию-член, которая сообщит, какая сейчас используется картинка.) *Функции, предоставляющие <STRONG>строгую гарантию,</STRONG> обещают, что если исключение будет возбуждено, то состояние программы не изменится. Вызов такой функции является атомарным; если он завершился успешно, то все запланированные действия выполнены до конца, если же нет, то программа останется в таком состоянии, как будто функция никогда не вызывалась. Работать с функциями, представляющими такую гарантию, проще, чем с функциями, которые дают только базовую гарантию, потому что после их вызова может быть только два состояния программы: то, которое ожидается в результате ее успешного завершения, и то, которое было до ее вызова. Напротив, если исключение возникает в функции, представляющей только базовую гарантию, то программа может оказаться в <EM>любом</EM> корректном состоянии. *Функции, предоставляющие <STRONG>гарантию отсутствия исключений,</STRONG> обещают никогда не возбуждать исключений, потому что всегда делают то, что должны делать. Все операции над встроенными типами (например, целыми, указателями и т. п.) обеспечивают такую гарантию. Это основной строительный блок безопасного относительно исключений кода. <P>Разумно предположить, что функции с пустой спецификацией исключений не возбуждают их, но это не всегда так. Например, рассмотрим следующую функцию:</P> <source lang="cpp"> int doSomethmg () throw(); // обратите внимание на пустую // спецификацию исключений </source> <P>Это объявление не говорит о том, что doSomething никогда не возбуждает исключений. Утверждается лишь, что <EM>если</EM> doSomething возбудит исключение, значит, произошла серьезная ошибка и должна быть вызвана функция unexpected<A HREF="#n_3" onmouseover="ShowBookNote('n_3')" onmouseout="HideBookNote('n_3')"><SUP>[3]</SUP></A>. Фактически doSomething может вообще не представлять никаких гарантий относительно исключений. Объявление функции (включающее ее спецификацию исключений) ничего не сообщает относительно того, является ли она корректной, переносима, эффективной, какие гарантии безопасности исключений она предоставляет и предоставляет ли их вообще. Все эти характеристики определяются реализацией функции, а не ее объявлением.</P> <P>Безопасный относительно исключений код должен представлять одну из трех описанных гарантий. Если он этого не делает, он не является безопасным. Выбор, таким образом, в том, чтобы определить, какой тип гарантии должна представлять каждая из написанных вами функций. Если не считать унаследованный код, небезопасный относительно исключений (об этом мы поговорим далее в настоящем правиле), то отсутствие гарантий допустимо лишь, если в результате анализа требований было решено, что приложение просто обязано допускать утечку ресурсов и работать с поврежденными структурами данных.</P> <P>Вообще говоря, нужно стремиться предоставить максимально строгие гарантии. С точки зрения безопасности исключений функции, не возбуждающие исключений, чудесны, но очень трудно, не оставаясь в рамках языка C, обойтись без вызова функций, возбуждающих исключения. Любой класс, в котором используется динамическое распределение памяти (например, STL-контейнеры), может возбуждать исключение bad_alloc, когда не удается найти достаточного объема свободной памяти ([[Правило 49: Разберитесь в поведении обработчика new | см. правило 49]]). Предоставляйте гарантии отсутствия исключений, когда можете, но для большинства функций есть только выбор между базовой и строгой гарантией.</P> <P>Для функции changeBackground предоставить <EM>почти</EM> строгую гарантию нетрудно. Во-первых, измените тип данных bgImage в классе PrettyMenu со встроенного указателя *Image на один из «интеллектуальных» управляющих ресурсами указателей, описанных в [[Правило 13: Используйте объекты для управления ресурсами | правиле 13]]. Откровенно говоря, это в любом случае неплохо, поскольку позволяет избежать утечек ресурсов. Тот факт, что это заодно помогает обеспечить строгую гарантию безопасности исключений, просто подтверждает приведенные в [[Правило 13: Используйте объекты для управления ресурсами | правиле 13]] аргументы в пользу применения объектов (наподобие интеллектуальных указателей) для управления ресурсами. Ниже я воспользовался классом tr1::shared_ptr, потому что он ведет себя более естественно при копировании, чем auto_ptr.</P> <P>Во-вторых, нужно изменить порядок предложений в функции changeBackground так, чтобы значение счетчика imageChanges не увеличивалось до тех пор, пока картинка не будет заменена. Общее правило таково: помечайте в объекте, что произошло некоторое изменение, только после того, как это изменение действительно выполнено.</P> <P>Вот что получается в результате:</P> <source lang="cpp"> class PrettyMenu { ... std::trl::shared_ptr<Image> bglmage; ... void PrettyMenu::changeBackground(std::lstreamS lmgSrc) { Lock ml(mutex); Bglmage.reset(new Image(imgSrc)); // заменить внутренний указатель // bglmage результатом выражения "new Image" ++imageChanges; } </source> <P>Отметим, что больше нет необходимости вручную удалять старую картинку, потому что это делает «интеллектуальный» указатель. Более того, удаление происходит только в том случае, если новая картинка успешно создана. Точнее говоря, функция tr1::shared_ptr::reset будет вызвана, только в том случае, когда ее параметр (результат вычисления «new Image(imgSrc)») успешно создан. Оператор delete используется только внутри вызова reset, поэтому если функция не получает управления, то и delete не вызывается. Отметим также, что использование объекта (tr1::shared_ptr) для управления ресурсом (динамически выделенным объектом Image) ко всему прочему уменьшает размер функции changeBackground.</P> <P>Как я сказал, эти два изменения позволяют changeBackground предоставлять <EM>почти</EM> строгую гарантию безопасности исключений. Так чего же не хватает? Дело в параметре imgSrc. Если конструктор Image возбудит исключение, может случиться, что указатель чтения из входного потока сместится, и такое смещение может оказаться изменением состояния, видимым остальной части программы. До тех пор пока у функции changeBackground есть этот недостаток, она предоставляет только базовую гарантию безопасности исключений.</P> <P>Но оставим в стороне этот нюанс и будем считать, что changeBackground представляет строгую гарантию безопасности. (По секрету сообщу, что есть способ добиться этого, изменив тип параметра с istream на имя файла, содержащего данные картинки.) Существует общая стратегия проектирования, которая обеспечивает строгую гарантию, и важно ее знать. Стратегия называется «скопировать и обменять» (copy and swap). В принципе, это очень просто. Сделайте копию объекта, который собираетесь модифицировать, затем внесите все необходимые изменения в копию. Если любая из операций модификации возбудит исключение, исходный объект останется неизменным. Когда все изменения будут успешно внесены, обменяйте модифицированный объект с исходным с помощью операции, не возбуждающей исключений.</P> <P>Обычно это реализуется помещением всех имеющих отношение к объекту данных из «реального» объекта в отдельный внутренний объект, на который в «реальном» объекте имеется указатель. Часто этот прием называют «идиома pimpl», и [[Правило 31: Уменьшайте зависимости файлов при компиляции | в правиле 31]] он описывается более подробно. Для класса PrettyMenu это может выглядеть примерно так:</P> <BR><P><CODE>struct PMImpl { // PMImpl = “PrettyMenu Impl”:</CODE></P> <P><CODE>std::tr1::shared_ptr&lt;Image&gt; bgImage; // см. далее – почему это</CODE></P> <P><CODE>int imageChanges; // структура, а не класс</CODE></P> <P><CODE>}</CODE></P> <P><CODE>class PrettyMenu {</CODE></P> <P><CODE>...</CODE></P> <P><CODE>private:</CODE></P> <P><CODE>Mutex mutex;</CODE></P> <P><CODE>std::tr1::shared_ptr&lt;PMImpl&gt; pimpl;</CODE></P> <P><CODE>};</CODE></P> <P><CODE>void PrettyMenu::changeBackground(std::istream&amp; imgSrc)</CODE></P> <P><CODE>{</CODE></P> <P><CODE>using std::swap; // см. правило 25</CODE></P> <P><CODE>Lock ml(&amp;mutex); // захватить мьютекс</CODE></P> <P><CODE>std::tr1::shared_ptr&lt;PMImpl&gt; // копировать данные obj</CODE></P> <P><CODE>pNew(new PMImpl(*pimpl));</CODE></P> <P><CODE>pNew-&gt;bgImage.reset(new Image(imgSrc)); // модифицировать копию</CODE></P> <P><CODE>++pNew-&gt;imageChanges;</CODE></P> <P><CODE>swap(pimpl, pNew); // обменять значения</CODE></P> <P><CODE>} // освободить мьютекс</CODE></P> <BR><P>В этом примере я решил сделать PMImpl структурой, а не классом, потому что инкапсуляция данных PrettyMenu достигается за счет того, что член pImpl объявлен закрытым. Объявить PMImpl классом было бы ничем не хуже, хотя и менее удобно (зато поборники «объектно-ориентированной чистоты» были бы довольны). Если нужно, PMImpl можно поместить внутрь PrettyMenu, но такое перемещение никак не влияет на написание безопасного относительно исключений кода.</P> <P>Стратегия копирования и обмена – это отличный способ внести изменения в состояние объекта по принципу «все или ничего», но в общем случае при этом не гарантируется, что вся функция в целом строго безопасна относительно исключений. Чтобы понять почему, абстрагируемся от функции changeBackground и рассмотрим вместо нее некоторую функцию someFunc, которая использует копирование с обменом, но еще и обращается к двум другим функциям: f1 и f2.</P> <BR><P><CODE>void someFunc()</CODE></P> <P><CODE>{</CODE></P> <P><CODE>... // скопировать локальное состояние</CODE></P> <P><CODE>f1();</CODE></P> <P><CODE>f2();</CODE></P> <P><CODE>... // обменять модифицированное состояние с копией</CODE></P> <P><CODE>}</CODE></P> <BR><P>Должно быть ясно, что если f1 или f2 не обеспечивают строгих гарантий безопасности исключений, то будет трудно обеспечить ее и для someFunc в целом. Например, предположим, что f1 обеспечивает только базовую гарантию. Чтобы someFunc обеспечивала строгую гарантию, необходимо написать код, определяющий состояние всей программы до вызова f1, перехватить все исключения, которые может возбудить f1, а затем восстановить исходное состояние.</P> <P>Ситуация не становится существенно лучше, если и f1, и f2 обеспечивают строгую гарантию безопасности исключений. Ведь если f1 нормально доработает до конца, состояние программы может измениться произвольным образом, поэтому если f2 возбудит исключение, то состояние программы не будет тем же, как перед вызовом someFunc, даже если f2 не изменит ничего.</P> <P>Проблема в побочных эффектах. До тех пор пока функция оперирует только локальным состоянием (то есть someFunc влияет только на состояние объекта, для которого вызвана), относительно легко обеспечить строгую гарантию. Но когда функция имеет побочные эффекты, затрагивающие нелокальные данные, все становится сложнее. Если, например, побочным эффектом вызова f1 является модификация базы данных, будет трудно обеспечить строгую гарантию для someFunc. Не существует способа отменить модификацию базы данных, которая уже была совершена: другие клиенты могли уже увидеть новое состояние.</P> <P>Подобные ситуации могут помешать предоставлению строгой гарантии безопасности для функции, даже если вы хотели бы это сделать. Кроме того, надо принять во внимание эффективность. Смысл «копирования и обмена» в том, чтобы модифицировать копию данных объекта, а затем обменять модифицированные и исходные данные операцией, которая не возбуждает исключений. Для этого нужно сделать копию каждого объекта, который подлежит модификации, что потребует времени и памяти, которыми вы, возможно, не располагаете. Строгая гарантия весьма желательна, и вы должны обеспечивать ее, когда это разумно и практично, но не обязательно во всех случаях.</P> <P>Когда невозможно предоставить строгую гарантию, вы должны обеспечить базовую. На практике может оказаться так, что для некоторых функций можно обеспечить строгую гарантию, тогда как для многих других это неразумно из соображений эффективности и сложности. Если вы сделали все возможное для обеспечения строгой гарантии там, где это оправдано, никто не вправе критиковать вас за то, что в остальных случаях вы представляете только базовую гарантию. Для многих функций базовая гарантия – совершенно разумный выбор.</P> <P>Совсем другое дело, если вы пишете функцию, которая вообще не представляет никаких гарантий безопасности исключений. Тут вступает в силу презумпция виновности: подсудимый считается виновным, пока не докажет обратного. Вы <EM>должны</EM> писать код, безопасный относительно исключений. Однако у вас есть право на защиту. Рассмотрим еще раз реализацию функции someFunc, которая вызывает f1 и f2. Предположим, что f2 не представляет никаких гарантий безопасности исключений, даже базовой. Это значит, что если f2 возбудит исключение, то возможна утечка ресурсов внутри f2. Это также означает, что f2 может повредить структуры данных, например отсортированные массивы могут стать неотсортированными, объект, который копировался из одной структуры в другую, может потеряться и т. д. Функция someFunc ничего не может с этим поделать. Если вызываемые из someFunc функции не гарантируют безопасности относительно исключений, то и someFunc не может предоставить никаких гарантий.</P> <P>Вот теперь мы можем вернуться к теме беременности. Женщина либо беременна, либо нет. Невозможно быть чуть-чуть беременной. Аналогично программная система является либо безопасной по исключениям, либо нет. Нет такого понятия, как частично безопасная система. Если система имеет всего одну небезопасную относительно исключений функцию, то она небезопасна и в целом, потому что вызов этой функции может привести к утечке ресурсов и повреждению структур данных. К несчастью, большинство унаследованного кода на C++ было написано без учета требований безопасности исключений, поэтому многие системы на сегодня являются в этом отношении небезопасными. Они включают код, написанный в небезопасной манере.</P> <P>Но нет причин сохранять такое положение дел навсегда. При написании нового кода или модификации существующего тщательно продумывайте способы достижения безопасности исключений. Начните с применения объектов управления ресурсами (см. правило 13). Это предотвратит утечку ресурсов. Затем определите, какую максимальную из трех гарантий безопасности исключений вы можете обеспечить для разрабатываемых функций, оставляя их небезопасными только в том случае, когда вызовы унаследованного кода не оставляют другого выбора. Документируйте ваши решения как для пользователей ваших функций, так и для сопровождения в будущем. Гарантия безопасности исключений функции – это видимая часть ее интерфейса, поэтому вы должны подходить к ней столь же ответственно, как и к другим аспектам интерфейса.</P> <P>Сорок лет назад код, изобилующий операторами goto, считался вполне приемлемым. Теперь же мы стараемся писать структурированные программы. Двенадцать лет назад глобальные данные ни у кого не вызывали возражений. Теперь мы стремимся данные инкапсулировать. Десять лет назад написание функций без учета влияния исключений было нормой. А сейчас мы боремся за достижение безопасности относительно исключений.</P> <P>Времена меняются. Мы живем. Мы учимся.</P> <H2><STRONG><EM><a name=label75 style="border:none;"></a>Что следует помнить</EM></STRONG></H2><P>• Безопасные относительно исключений функции не допускают утечки ресурсов и повреждения структур данных, даже в случае возбуждения исключений. Такие функции предоставляют базовую гарантию, строгую гарантию либо гарантию полного отсутствия исключений.</P> <P>• Строгая гарантия часто может быть реализована посредством копирования и обмена, но предоставлять ее для всех функций непрактично.</P> <P>• Функция обычно может предоставить гарантию не строже, чем самая слабая гарантия, обеспечиваемая вызываемыми из нее функциями. 1da0d9de11dbc735685af846a37f399a1c655d0e 59 58 2013-07-11T12:50:36Z Lerom 3360334 wikitext text/x-wiki <P>Безопасность исключений в чем-то подобна беременности… но пока отложим эту мысль в сторонку. Нельзя всерьез говорить о репродуктивной функции, пока не завершился этап ухаживания.</P> <P>Предположим, что у нас есть класс, представляющий меню с фоновыми картинками в графическом интерфейсе пользователя. Этот класс предназначен для использования в многопоточной среде, поэтому он включает мьютекс для синхронизации доступа:</P> <source lang="cpp"> class PrettyMenu { public: ... void changeBackground(std::istream& imgSrc); // сменить фоновую ... // картинку private: Mutex mutex; // мьютекс объекта Image *bgImage; // текущая фоновая картинка int imageChanges; // сколько раз картинка менялась }; </source> <P>Рассмотрим следующую возможную реализацию функции-члена change-Background:</P> <source lang="cpp"> void PrettyMenu::changeBackground(std::istreamS lmgSrc) { lock(Smutex); // захватить мьютекс delete bglmage; // избавиться от старой картинки ++imageChanges; // обновить счетчик изменений картинки bglmage = new Image(lmgSrc); // установить новый фон unlock(Smutex); // освободить мьютекс } </source> <P>С точки зрения безопасности исключений, эта функция настолько плоха, насколько вообще возможно. К безопасности исключений предъявляется два требования, и она не удовлетворяет ни одному из них.</P> <P>Когда возбуждается исключение, то безопасная относительно исключений функция:</P> *<STRONG>Не допускает утечки ресурсов.</STRONG> Приведенный код не проходит этот тест, потому что если выражение «new Image(imgSrc)» возбудит исключение, то вызов unlock никогда не выполнится, и мьютекс окажется захваченным навсегда. *<STRONG>Не допускает повреждения структур данных.</STRONG> Если «new Image(imgSrc)» возбудит исключение, в bgImage останется указатель на удаленный объект. Кроме того, счетчик imageChanges увеличивается, несмотря на то что новая картинка не установлена. (С другой стороны, старая картинка уже полностью удалена, так что трудно сделать вид, будто ничего не изменилось.) <P>Справиться с утечкой ресурсов легко – в [[Правило 13: Используйте объекты для управления ресурсами | правиле 13]] объяснено, как пользоваться объектами, управляющими ресурсами, а в [[Правило 14: Тщательно продумывайте поведение при копировании классов, управляющих ресурсами | правиле 14]] представлен класс Lock, гарантирующий своеременное освобождение мьютексов:</P> <source lang="cpp"> void PrettyMenu::changeBackground(std::istreamS lmgSrc) { Lock ml(mutex); // из правила 14: захватить мьютекс // и гарантировать его последующее освобождение delete bglmage; ++imageChanges; bglmage = new Image(lmgSrc); } </source> <P>Одним из преимуществ классов для управления ресурсами, подобных Lock, является то, что обычно они уменьшают размер функций. Заметили, что вызов unlock уже не нужен? Общее правило гласит: чем меньше кода, тем лучше, потому что меньше возможностей для ошибок и меньше путаницы при внесении изменений.</P> <P>От утечки ресурсов перейдем к проблеме возможного повреждения данных. Здесь у нас есть выбор, но прежде чем его сделать, нужно уточнить терминологию.</P> <P>Безопасные относительно исключений функции предоставляют одну из трех гарантий.</P> *Функции, предоставляющие <STRONG>базовую гарантию,</STRONG> обещают, что если исключение будет возбуждено, то все в программе остается в корректном состоянии. Никакие объекты или структуры данных не повреждены, и все объекты находятся в непротиворечивом состоянии (например, все инварианты классов не нарушены). Однако точное состояние программы может быть непредсказуемо. Например, мы можем написать функцию change-Background так, что при возникновении исключения объект PrettyMenu сохранит старую фоновую картинку либо у него будет какой-то фон по умолчанию, но пользователи не могут заранее знать, какой. (Чтобы выяснить это, им придется вызвать какую-то функцию-член, которая сообщит, какая сейчас используется картинка.) *Функции, предоставляющие <STRONG>строгую гарантию,</STRONG> обещают, что если исключение будет возбуждено, то состояние программы не изменится. Вызов такой функции является атомарным; если он завершился успешно, то все запланированные действия выполнены до конца, если же нет, то программа останется в таком состоянии, как будто функция никогда не вызывалась. Работать с функциями, представляющими такую гарантию, проще, чем с функциями, которые дают только базовую гарантию, потому что после их вызова может быть только два состояния программы: то, которое ожидается в результате ее успешного завершения, и то, которое было до ее вызова. Напротив, если исключение возникает в функции, представляющей только базовую гарантию, то программа может оказаться в <EM>любом</EM> корректном состоянии. *Функции, предоставляющие <STRONG>гарантию отсутствия исключений,</STRONG> обещают никогда не возбуждать исключений, потому что всегда делают то, что должны делать. Все операции над встроенными типами (например, целыми, указателями и т. п.) обеспечивают такую гарантию. Это основной строительный блок безопасного относительно исключений кода. <P>Разумно предположить, что функции с пустой спецификацией исключений не возбуждают их, но это не всегда так. Например, рассмотрим следующую функцию:</P> <source lang="cpp"> int doSomethmg () throw(); // обратите внимание на пустую // спецификацию исключений </source> <P>Это объявление не говорит о том, что doSomething никогда не возбуждает исключений. Утверждается лишь, что <EM>если</EM> doSomething возбудит исключение, значит, произошла серьезная ошибка и должна быть вызвана функция unexpected (более подробную информацию о функции unexpected вы можете найти, воспользовавшись поисковым сервисом или в полном руководстве по языку C++ (возможно, стоит поискать информацию о функции set_unexpected, которая специфицирует unexpected)). Фактически doSomething может вообще не представлять никаких гарантий относительно исключений. Объявление функции (включающее ее спецификацию исключений) ничего не сообщает относительно того, является ли она корректной, переносима, эффективной, какие гарантии безопасности исключений она предоставляет и предоставляет ли их вообще. Все эти характеристики определяются реализацией функции, а не ее объявлением.</P> <P>Безопасный относительно исключений код должен представлять одну из трех описанных гарантий. Если он этого не делает, он не является безопасным. Выбор, таким образом, в том, чтобы определить, какой тип гарантии должна представлять каждая из написанных вами функций. Если не считать унаследованный код, небезопасный относительно исключений (об этом мы поговорим далее в настоящем правиле), то отсутствие гарантий допустимо лишь, если в результате анализа требований было решено, что приложение просто обязано допускать утечку ресурсов и работать с поврежденными структурами данных.</P> <P>Вообще говоря, нужно стремиться предоставить максимально строгие гарантии. С точки зрения безопасности исключений функции, не возбуждающие исключений, чудесны, но очень трудно, не оставаясь в рамках языка C, обойтись без вызова функций, возбуждающих исключения. Любой класс, в котором используется динамическое распределение памяти (например, STL-контейнеры), может возбуждать исключение bad_alloc, когда не удается найти достаточного объема свободной памяти ([[Правило 49: Разберитесь в поведении обработчика new | см. правило 49]]). Предоставляйте гарантии отсутствия исключений, когда можете, но для большинства функций есть только выбор между базовой и строгой гарантией.</P> <P>Для функции changeBackground предоставить <EM>почти</EM> строгую гарантию нетрудно. Во-первых, измените тип данных bgImage в классе PrettyMenu со встроенного указателя *Image на один из «интеллектуальных» управляющих ресурсами указателей, описанных в [[Правило 13: Используйте объекты для управления ресурсами | правиле 13]]. Откровенно говоря, это в любом случае неплохо, поскольку позволяет избежать утечек ресурсов. Тот факт, что это заодно помогает обеспечить строгую гарантию безопасности исключений, просто подтверждает приведенные в [[Правило 13: Используйте объекты для управления ресурсами | правиле 13]] аргументы в пользу применения объектов (наподобие интеллектуальных указателей) для управления ресурсами. Ниже я воспользовался классом tr1::shared_ptr, потому что он ведет себя более естественно при копировании, чем auto_ptr.</P> <P>Во-вторых, нужно изменить порядок предложений в функции changeBackground так, чтобы значение счетчика imageChanges не увеличивалось до тех пор, пока картинка не будет заменена. Общее правило таково: помечайте в объекте, что произошло некоторое изменение, только после того, как это изменение действительно выполнено.</P> <P>Вот что получается в результате:</P> <source lang="cpp"> class PrettyMenu { ... std::trl::shared_ptr<Image> bglmage; ... void PrettyMenu::changeBackground(std::lstreamS lmgSrc) { Lock ml(mutex); Bglmage.reset(new Image(imgSrc)); // заменить внутренний указатель // bglmage результатом выражения "new Image" ++imageChanges; } </source> <P>Отметим, что больше нет необходимости вручную удалять старую картинку, потому что это делает «интеллектуальный» указатель. Более того, удаление происходит только в том случае, если новая картинка успешно создана. Точнее говоря, функция tr1::shared_ptr::reset будет вызвана, только в том случае, когда ее параметр (результат вычисления «new Image(imgSrc)») успешно создан. Оператор delete используется только внутри вызова reset, поэтому если функция не получает управления, то и delete не вызывается. Отметим также, что использование объекта (tr1::shared_ptr) для управления ресурсом (динамически выделенным объектом Image) ко всему прочему уменьшает размер функции changeBackground.</P> <P>Как я сказал, эти два изменения позволяют changeBackground предоставлять <EM>почти</EM> строгую гарантию безопасности исключений. Так чего же не хватает? Дело в параметре imgSrc. Если конструктор Image возбудит исключение, может случиться, что указатель чтения из входного потока сместится, и такое смещение может оказаться изменением состояния, видимым остальной части программы. До тех пор пока у функции changeBackground есть этот недостаток, она предоставляет только базовую гарантию безопасности исключений.</P> <P>Но оставим в стороне этот нюанс и будем считать, что changeBackground представляет строгую гарантию безопасности. (По секрету сообщу, что есть способ добиться этого, изменив тип параметра с istream на имя файла, содержащего данные картинки.) Существует общая стратегия проектирования, которая обеспечивает строгую гарантию, и важно ее знать. Стратегия называется «скопировать и обменять» (copy and swap). В принципе, это очень просто. Сделайте копию объекта, который собираетесь модифицировать, затем внесите все необходимые изменения в копию. Если любая из операций модификации возбудит исключение, исходный объект останется неизменным. Когда все изменения будут успешно внесены, обменяйте модифицированный объект с исходным с помощью операции, не возбуждающей исключений.</P> <P>Обычно это реализуется помещением всех имеющих отношение к объекту данных из «реального» объекта в отдельный внутренний объект, на который в «реальном» объекте имеется указатель. Часто этот прием называют «идиома pimpl», и [[Правило 31: Уменьшайте зависимости файлов при компиляции | в правиле 31]] он описывается более подробно. Для класса PrettyMenu это может выглядеть примерно так:</P> <source lang="cpp"> struct PMImpl { // PMImpl = “PrettyMenu Impl”: std::tr1::shared_ptr<Image> bgImage; // см. далее – почему это int imageChanges; // структура, а не класс } class PrettyMenu { ... private: Mutex mutex; std::tr1::shared_ptr<PMImpl> pimpl; }; void PrettyMenu::changeBackground(std::istream& imgSrc) { using std::swap; // см. правило 25 Lock ml(&mutex); // захватить мьютекс std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pimpl)); // копировать данные obj pNew->bgImage.reset(new Image(imgSrc)); // модифицировать копию ++pNew->imageChanges; swap(pimpl, pNew); // обменять значения } // освободить мьютекс </source> <P>В этом примере я решил сделать PMImpl структурой, а не классом, потому что инкапсуляция данных PrettyMenu достигается за счет того, что член pImpl объявлен закрытым. Объявить PMImpl классом было бы ничем не хуже, хотя и менее удобно (зато поборники «объектно-ориентированной чистоты» были бы довольны). Если нужно, PMImpl можно поместить внутрь PrettyMenu, но такое перемещение никак не влияет на написание безопасного относительно исключений кода.</P> <P>Стратегия копирования и обмена – это отличный способ внести изменения в состояние объекта по принципу «все или ничего», но в общем случае при этом не гарантируется, что вся функция в целом строго безопасна относительно исключений. Чтобы понять почему, абстрагируемся от функции changeBackground и рассмотрим вместо нее некоторую функцию someFunc, которая использует копирование с обменом, но еще и обращается к двум другим функциям: f1 и f2.</P> <source lang="cpp"> void someFunc() { ... // скопировать локальное состояние f1(); f2() ; ... // обменять модифицированное состояние с копией } </source> <P>Должно быть ясно, что если f1 или f2 не обеспечивают строгих гарантий безопасности исключений, то будет трудно обеспечить ее и для someFunc в целом. Например, предположим, что f1 обеспечивает только базовую гарантию. Чтобы someFunc обеспечивала строгую гарантию, необходимо написать код, определяющий состояние всей программы до вызова f1, перехватить все исключения, которые может возбудить f1, а затем восстановить исходное состояние.</P> <P>Ситуация не становится существенно лучше, если и f1, и f2 обеспечивают строгую гарантию безопасности исключений. Ведь если f1 нормально доработает до конца, состояние программы может измениться произвольным образом, поэтому если f2 возбудит исключение, то состояние программы не будет тем же, как перед вызовом someFunc, даже если f2 не изменит ничего.</P> <P>Проблема в побочных эффектах. До тех пор пока функция оперирует только локальным состоянием (то есть someFunc влияет только на состояние объекта, для которого вызвана), относительно легко обеспечить строгую гарантию. Но когда функция имеет побочные эффекты, затрагивающие нелокальные данные, все становится сложнее. Если, например, побочным эффектом вызова f1 является модификация базы данных, будет трудно обеспечить строгую гарантию для someFunc. Не существует способа отменить модификацию базы данных, которая уже была совершена: другие клиенты могли уже увидеть новое состояние.</P> <P>Подобные ситуации могут помешать предоставлению строгой гарантии безопасности для функции, даже если вы хотели бы это сделать. Кроме того, надо принять во внимание эффективность. Смысл «копирования и обмена» в том, чтобы модифицировать копию данных объекта, а затем обменять модифицированные и исходные данные операцией, которая не возбуждает исключений. Для этого нужно сделать копию каждого объекта, который подлежит модификации, что потребует времени и памяти, которыми вы, возможно, не располагаете. Строгая гарантия весьма желательна, и вы должны обеспечивать ее, когда это разумно и практично, но не обязательно во всех случаях.</P> <P>Когда невозможно предоставить строгую гарантию, вы должны обеспечить базовую. На практике может оказаться так, что для некоторых функций можно обеспечить строгую гарантию, тогда как для многих других это неразумно из соображений эффективности и сложности. Если вы сделали все возможное для обеспечения строгой гарантии там, где это оправдано, никто не вправе критиковать вас за то, что в остальных случаях вы представляете только базовую гарантию. Для многих функций базовая гарантия – совершенно разумный выбор.</P> <P>Совсем другое дело, если вы пишете функцию, которая вообще не представляет никаких гарантий безопасности исключений. Тут вступает в силу презумпция виновности: подсудимый считается виновным, пока не докажет обратного. Вы <EM>должны</EM> писать код, безопасный относительно исключений. Однако у вас есть право на защиту. Рассмотрим еще раз реализацию функции someFunc, которая вызывает f1 и f2. Предположим, что f2 не представляет никаких гарантий безопасности исключений, даже базовой. Это значит, что если f2 возбудит исключение, то возможна утечка ресурсов внутри f2. Это также означает, что f2 может повредить структуры данных, например отсортированные массивы могут стать неотсортированными, объект, который копировался из одной структуры в другую, может потеряться и т. д. Функция someFunc ничего не может с этим поделать. Если вызываемые из someFunc функции не гарантируют безопасности относительно исключений, то и someFunc не может предоставить никаких гарантий.</P> <P>Вот теперь мы можем вернуться к теме беременности. Женщина либо беременна, либо нет. Невозможно быть чуть-чуть беременной. Аналогично программная система является либо безопасной по исключениям, либо нет. Нет такого понятия, как частично безопасная система. Если система имеет всего одну небезопасную относительно исключений функцию, то она небезопасна и в целом, потому что вызов этой функции может привести к утечке ресурсов и повреждению структур данных. К несчастью, большинство унаследованного кода на C++ было написано без учета требований безопасности исключений, поэтому многие системы на сегодня являются в этом отношении небезопасными. Они включают код, написанный в небезопасной манере.</P> <P>Но нет причин сохранять такое положение дел навсегда. При написании нового кода или модификации существующего тщательно продумывайте способы достижения безопасности исключений. Начните с применения объектов управления ресурсами ([[Правило 13: Используйте объекты для управления ресурсами | см. правило 13]]). Это предотвратит утечку ресурсов. Затем определите, какую максимальную из трех гарантий безопасности исключений вы можете обеспечить для разрабатываемых функций, оставляя их небезопасными только в том случае, когда вызовы унаследованного кода не оставляют другого выбора. Документируйте ваши решения как для пользователей ваших функций, так и для сопровождения в будущем. Гарантия безопасности исключений функции – это видимая часть ее интерфейса, поэтому вы должны подходить к ней столь же ответственно, как и к другим аспектам интерфейса.</P> <P>Сорок лет назад код, изобилующий операторами goto, считался вполне приемлемым. Теперь же мы стараемся писать структурированные программы. Двенадцать лет назад глобальные данные ни у кого не вызывали возражений. Теперь мы стремимся данные инкапсулировать. Десять лет назад написание функций без учета влияния исключений было нормой. А сейчас мы боремся за достижение безопасности относительно исключений.</P> <P>Времена меняются. Мы живем. Мы учимся.</P> Что следует помнить *Безопасные относительно исключений функции не допускают утечки ресурсов и повреждения структур данных, даже в случае возбуждения исключений. Такие функции предоставляют базовую гарантию, строгую гарантию либо гарантию полного отсутствия исключений. *Строгая гарантия часто может быть реализована посредством копирования и обмена, но предоставлять ее для всех функций непрактично. *Функция обычно может предоставить гарантию не строже, чем самая слабая гарантия, обеспечиваемая вызываемыми из нее функциями. db86d4d5a943d58c132c95818f2ba28cc911680f 60 59 2013-07-11T12:51:30Z Lerom 3360334 wikitext text/x-wiki <P>Безопасность исключений в чем-то подобна беременности… но пока отложим эту мысль в сторонку. Нельзя всерьез говорить о репродуктивной функции, пока не завершился этап ухаживания.</P> <P>Предположим, что у нас есть класс, представляющий меню с фоновыми картинками в графическом интерфейсе пользователя. Этот класс предназначен для использования в многопоточной среде, поэтому он включает мьютекс для синхронизации доступа:</P> <source lang="cpp"> class PrettyMenu { public: ... void changeBackground(std::istream& imgSrc); // сменить фоновую ... // картинку private: Mutex mutex; // мьютекс объекта Image *bgImage; // текущая фоновая картинка int imageChanges; // сколько раз картинка менялась }; </source> <P>Рассмотрим следующую возможную реализацию функции-члена change-Background:</P> <source lang="cpp"> void PrettyMenu::changeBackground(std::istreamS lmgSrc) { lock(Smutex); // захватить мьютекс delete bglmage; // избавиться от старой картинки ++imageChanges; // обновить счетчик изменений картинки bglmage = new Image(lmgSrc); // установить новый фон unlock(Smutex); // освободить мьютекс } </source> <P>С точки зрения безопасности исключений, эта функция настолько плоха, насколько вообще возможно. К безопасности исключений предъявляется два требования, и она не удовлетворяет ни одному из них.</P> <P>Когда возбуждается исключение, то безопасная относительно исключений функция:</P> *<STRONG>Не допускает утечки ресурсов.</STRONG> Приведенный код не проходит этот тест, потому что если выражение «new Image(imgSrc)» возбудит исключение, то вызов unlock никогда не выполнится, и мьютекс окажется захваченным навсегда. *<STRONG>Не допускает повреждения структур данных.</STRONG> Если «new Image(imgSrc)» возбудит исключение, в bgImage останется указатель на удаленный объект. Кроме того, счетчик imageChanges увеличивается, несмотря на то что новая картинка не установлена. (С другой стороны, старая картинка уже полностью удалена, так что трудно сделать вид, будто ничего не изменилось.) <P>Справиться с утечкой ресурсов легко – в [[Правило 13: Используйте объекты для управления ресурсами | правиле 13]] объяснено, как пользоваться объектами, управляющими ресурсами, а в [[Правило 14: Тщательно продумывайте поведение при копировании классов, управляющих ресурсами | правиле 14]] представлен класс Lock, гарантирующий своеременное освобождение мьютексов:</P> <source lang="cpp"> void PrettyMenu::changeBackground(std::istreamS lmgSrc) { Lock ml(mutex); // из правила 14: захватить мьютекс // и гарантировать его последующее освобождение delete bglmage; ++imageChanges; bglmage = new Image(lmgSrc); } </source> <P>Одним из преимуществ классов для управления ресурсами, подобных Lock, является то, что обычно они уменьшают размер функций. Заметили, что вызов unlock уже не нужен? Общее правило гласит: чем меньше кода, тем лучше, потому что меньше возможностей для ошибок и меньше путаницы при внесении изменений.</P> <P>От утечки ресурсов перейдем к проблеме возможного повреждения данных. Здесь у нас есть выбор, но прежде чем его сделать, нужно уточнить терминологию.</P> <P>Безопасные относительно исключений функции предоставляют одну из трех гарантий.</P> *Функции, предоставляющие <STRONG>базовую гарантию,</STRONG> обещают, что если исключение будет возбуждено, то все в программе остается в корректном состоянии. Никакие объекты или структуры данных не повреждены, и все объекты находятся в непротиворечивом состоянии (например, все инварианты классов не нарушены). Однако точное состояние программы может быть непредсказуемо. Например, мы можем написать функцию change-Background так, что при возникновении исключения объект PrettyMenu сохранит старую фоновую картинку либо у него будет какой-то фон по умолчанию, но пользователи не могут заранее знать, какой. (Чтобы выяснить это, им придется вызвать какую-то функцию-член, которая сообщит, какая сейчас используется картинка.) *Функции, предоставляющие <STRONG>строгую гарантию,</STRONG> обещают, что если исключение будет возбуждено, то состояние программы не изменится. Вызов такой функции является атомарным; если он завершился успешно, то все запланированные действия выполнены до конца, если же нет, то программа останется в таком состоянии, как будто функция никогда не вызывалась. Работать с функциями, представляющими такую гарантию, проще, чем с функциями, которые дают только базовую гарантию, потому что после их вызова может быть только два состояния программы: то, которое ожидается в результате ее успешного завершения, и то, которое было до ее вызова. Напротив, если исключение возникает в функции, представляющей только базовую гарантию, то программа может оказаться в <EM>любом</EM> корректном состоянии. *Функции, предоставляющие <STRONG>гарантию отсутствия исключений,</STRONG> обещают никогда не возбуждать исключений, потому что всегда делают то, что должны делать. Все операции над встроенными типами (например, целыми, указателями и т. п.) обеспечивают такую гарантию. Это основной строительный блок безопасного относительно исключений кода. <P>Разумно предположить, что функции с пустой спецификацией исключений не возбуждают их, но это не всегда так. Например, рассмотрим следующую функцию:</P> <source lang="cpp"> int doSomethmg () throw(); // обратите внимание на пустую // спецификацию исключений </source> <P>Это объявление не говорит о том, что doSomething никогда не возбуждает исключений. Утверждается лишь, что <EM>если</EM> doSomething возбудит исключение, значит, произошла серьезная ошибка и должна быть вызвана функция unexpected (более подробную информацию о функции unexpected вы можете найти, воспользовавшись поисковым сервисом или в полном руководстве по языку C++ (возможно, стоит поискать информацию о функции set_unexpected, которая специфицирует unexpected)). Фактически doSomething может вообще не представлять никаких гарантий относительно исключений. Объявление функции (включающее ее спецификацию исключений) ничего не сообщает относительно того, является ли она корректной, переносима, эффективной, какие гарантии безопасности исключений она предоставляет и предоставляет ли их вообще. Все эти характеристики определяются реализацией функции, а не ее объявлением.</P> <P>Безопасный относительно исключений код должен представлять одну из трех описанных гарантий. Если он этого не делает, он не является безопасным. Выбор, таким образом, в том, чтобы определить, какой тип гарантии должна представлять каждая из написанных вами функций. Если не считать унаследованный код, небезопасный относительно исключений (об этом мы поговорим далее в настоящем правиле), то отсутствие гарантий допустимо лишь, если в результате анализа требований было решено, что приложение просто обязано допускать утечку ресурсов и работать с поврежденными структурами данных.</P> <P>Вообще говоря, нужно стремиться предоставить максимально строгие гарантии. С точки зрения безопасности исключений функции, не возбуждающие исключений, чудесны, но очень трудно, не оставаясь в рамках языка C, обойтись без вызова функций, возбуждающих исключения. Любой класс, в котором используется динамическое распределение памяти (например, STL-контейнеры), может возбуждать исключение bad_alloc, когда не удается найти достаточного объема свободной памяти ([[Правило 49: Разберитесь в поведении обработчика new | см. правило 49]]). Предоставляйте гарантии отсутствия исключений, когда можете, но для большинства функций есть только выбор между базовой и строгой гарантией.</P> <P>Для функции changeBackground предоставить <EM>почти</EM> строгую гарантию нетрудно. Во-первых, измените тип данных bgImage в классе PrettyMenu со встроенного указателя *Image на один из «интеллектуальных» управляющих ресурсами указателей, описанных в [[Правило 13: Используйте объекты для управления ресурсами | правиле 13]]. Откровенно говоря, это в любом случае неплохо, поскольку позволяет избежать утечек ресурсов. Тот факт, что это заодно помогает обеспечить строгую гарантию безопасности исключений, просто подтверждает приведенные в [[Правило 13: Используйте объекты для управления ресурсами | правиле 13]] аргументы в пользу применения объектов (наподобие интеллектуальных указателей) для управления ресурсами. Ниже я воспользовался классом tr1::shared_ptr, потому что он ведет себя более естественно при копировании, чем auto_ptr.</P> <P>Во-вторых, нужно изменить порядок предложений в функции changeBackground так, чтобы значение счетчика imageChanges не увеличивалось до тех пор, пока картинка не будет заменена. Общее правило таково: помечайте в объекте, что произошло некоторое изменение, только после того, как это изменение действительно выполнено.</P> <P>Вот что получается в результате:</P> <source lang="cpp"> class PrettyMenu { ... std::trl::shared_ptr<Image> bglmage; ... void PrettyMenu::changeBackground(std::lstreamS lmgSrc) { Lock ml(mutex); Bglmage.reset(new Image(imgSrc)); // заменить внутренний указатель // bglmage результатом выражения "new Image" ++imageChanges; } </source> <P>Отметим, что больше нет необходимости вручную удалять старую картинку, потому что это делает «интеллектуальный» указатель. Более того, удаление происходит только в том случае, если новая картинка успешно создана. Точнее говоря, функция tr1::shared_ptr::reset будет вызвана, только в том случае, когда ее параметр (результат вычисления «new Image(imgSrc)») успешно создан. Оператор delete используется только внутри вызова reset, поэтому если функция не получает управления, то и delete не вызывается. Отметим также, что использование объекта (tr1::shared_ptr) для управления ресурсом (динамически выделенным объектом Image) ко всему прочему уменьшает размер функции changeBackground.</P> <P>Как я сказал, эти два изменения позволяют changeBackground предоставлять <EM>почти</EM> строгую гарантию безопасности исключений. Так чего же не хватает? Дело в параметре imgSrc. Если конструктор Image возбудит исключение, может случиться, что указатель чтения из входного потока сместится, и такое смещение может оказаться изменением состояния, видимым остальной части программы. До тех пор пока у функции changeBackground есть этот недостаток, она предоставляет только базовую гарантию безопасности исключений.</P> <P>Но оставим в стороне этот нюанс и будем считать, что changeBackground представляет строгую гарантию безопасности. (По секрету сообщу, что есть способ добиться этого, изменив тип параметра с istream на имя файла, содержащего данные картинки.) Существует общая стратегия проектирования, которая обеспечивает строгую гарантию, и важно ее знать. Стратегия называется «скопировать и обменять» (copy and swap). В принципе, это очень просто. Сделайте копию объекта, который собираетесь модифицировать, затем внесите все необходимые изменения в копию. Если любая из операций модификации возбудит исключение, исходный объект останется неизменным. Когда все изменения будут успешно внесены, обменяйте модифицированный объект с исходным с помощью операции, не возбуждающей исключений.</P> <P>Обычно это реализуется помещением всех имеющих отношение к объекту данных из «реального» объекта в отдельный внутренний объект, на который в «реальном» объекте имеется указатель. Часто этот прием называют «идиома pimpl», и [[Правило 31: Уменьшайте зависимости файлов при компиляции | в правиле 31]] он описывается более подробно. Для класса PrettyMenu это может выглядеть примерно так:</P> <source lang="cpp"> struct PMImpl { // PMImpl = “PrettyMenu Impl”: std::tr1::shared_ptr<Image> bgImage; // см. далее – почему это int imageChanges; // структура, а не класс } class PrettyMenu { ... private: Mutex mutex; std::tr1::shared_ptr<PMImpl> pimpl; }; void PrettyMenu::changeBackground(std::istream& imgSrc) { using std::swap; // см. правило 25 Lock ml(&mutex); // захватить мьютекс std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pimpl)); // копировать данные obj pNew->bgImage.reset(new Image(imgSrc)); // модифицировать копию ++pNew->imageChanges; swap(pimpl, pNew); // обменять значения } // освободить мьютекс </source> <P>В этом примере я решил сделать PMImpl структурой, а не классом, потому что инкапсуляция данных PrettyMenu достигается за счет того, что член pImpl объявлен закрытым. Объявить PMImpl классом было бы ничем не хуже, хотя и менее удобно (зато поборники «объектно-ориентированной чистоты» были бы довольны). Если нужно, PMImpl можно поместить внутрь PrettyMenu, но такое перемещение никак не влияет на написание безопасного относительно исключений кода.</P> <P>Стратегия копирования и обмена – это отличный способ внести изменения в состояние объекта по принципу «все или ничего», но в общем случае при этом не гарантируется, что вся функция в целом строго безопасна относительно исключений. Чтобы понять почему, абстрагируемся от функции changeBackground и рассмотрим вместо нее некоторую функцию someFunc, которая использует копирование с обменом, но еще и обращается к двум другим функциям: f1 и f2.</P> <source lang="cpp"> void someFunc() { ... // скопировать локальное состояние f1(); f2() ; ... // обменять модифицированное состояние с копией } </source> <P>Должно быть ясно, что если f1 или f2 не обеспечивают строгих гарантий безопасности исключений, то будет трудно обеспечить ее и для someFunc в целом. Например, предположим, что f1 обеспечивает только базовую гарантию. Чтобы someFunc обеспечивала строгую гарантию, необходимо написать код, определяющий состояние всей программы до вызова f1, перехватить все исключения, которые может возбудить f1, а затем восстановить исходное состояние.</P> <P>Ситуация не становится существенно лучше, если и f1, и f2 обеспечивают строгую гарантию безопасности исключений. Ведь если f1 нормально доработает до конца, состояние программы может измениться произвольным образом, поэтому если f2 возбудит исключение, то состояние программы не будет тем же, как перед вызовом someFunc, даже если f2 не изменит ничего.</P> <P>Проблема в побочных эффектах. До тех пор пока функция оперирует только локальным состоянием (то есть someFunc влияет только на состояние объекта, для которого вызвана), относительно легко обеспечить строгую гарантию. Но когда функция имеет побочные эффекты, затрагивающие нелокальные данные, все становится сложнее. Если, например, побочным эффектом вызова f1 является модификация базы данных, будет трудно обеспечить строгую гарантию для someFunc. Не существует способа отменить модификацию базы данных, которая уже была совершена: другие клиенты могли уже увидеть новое состояние.</P> <P>Подобные ситуации могут помешать предоставлению строгой гарантии безопасности для функции, даже если вы хотели бы это сделать. Кроме того, надо принять во внимание эффективность. Смысл «копирования и обмена» в том, чтобы модифицировать копию данных объекта, а затем обменять модифицированные и исходные данные операцией, которая не возбуждает исключений. Для этого нужно сделать копию каждого объекта, который подлежит модификации, что потребует времени и памяти, которыми вы, возможно, не располагаете. Строгая гарантия весьма желательна, и вы должны обеспечивать ее, когда это разумно и практично, но не обязательно во всех случаях.</P> <P>Когда невозможно предоставить строгую гарантию, вы должны обеспечить базовую. На практике может оказаться так, что для некоторых функций можно обеспечить строгую гарантию, тогда как для многих других это неразумно из соображений эффективности и сложности. Если вы сделали все возможное для обеспечения строгой гарантии там, где это оправдано, никто не вправе критиковать вас за то, что в остальных случаях вы представляете только базовую гарантию. Для многих функций базовая гарантия – совершенно разумный выбор.</P> <P>Совсем другое дело, если вы пишете функцию, которая вообще не представляет никаких гарантий безопасности исключений. Тут вступает в силу презумпция виновности: подсудимый считается виновным, пока не докажет обратного. Вы <EM>должны</EM> писать код, безопасный относительно исключений. Однако у вас есть право на защиту. Рассмотрим еще раз реализацию функции someFunc, которая вызывает f1 и f2. Предположим, что f2 не представляет никаких гарантий безопасности исключений, даже базовой. Это значит, что если f2 возбудит исключение, то возможна утечка ресурсов внутри f2. Это также означает, что f2 может повредить структуры данных, например отсортированные массивы могут стать неотсортированными, объект, который копировался из одной структуры в другую, может потеряться и т. д. Функция someFunc ничего не может с этим поделать. Если вызываемые из someFunc функции не гарантируют безопасности относительно исключений, то и someFunc не может предоставить никаких гарантий.</P> <P>Вот теперь мы можем вернуться к теме беременности. Женщина либо беременна, либо нет. Невозможно быть чуть-чуть беременной. Аналогично программная система является либо безопасной по исключениям, либо нет. Нет такого понятия, как частично безопасная система. Если система имеет всего одну небезопасную относительно исключений функцию, то она небезопасна и в целом, потому что вызов этой функции может привести к утечке ресурсов и повреждению структур данных. К несчастью, большинство унаследованного кода на C++ было написано без учета требований безопасности исключений, поэтому многие системы на сегодня являются в этом отношении небезопасными. Они включают код, написанный в небезопасной манере.</P> <P>Но нет причин сохранять такое положение дел навсегда. При написании нового кода или модификации существующего тщательно продумывайте способы достижения безопасности исключений. Начните с применения объектов управления ресурсами ([[Правило 13: Используйте объекты для управления ресурсами | см. правило 13]]). Это предотвратит утечку ресурсов. Затем определите, какую максимальную из трех гарантий безопасности исключений вы можете обеспечить для разрабатываемых функций, оставляя их небезопасными только в том случае, когда вызовы унаследованного кода не оставляют другого выбора. Документируйте ваши решения как для пользователей ваших функций, так и для сопровождения в будущем. Гарантия безопасности исключений функции – это видимая часть ее интерфейса, поэтому вы должны подходить к ней столь же ответственно, как и к другим аспектам интерфейса.</P> <P>Сорок лет назад код, изобилующий операторами goto, считался вполне приемлемым. Теперь же мы стараемся писать структурированные программы. Двенадцать лет назад глобальные данные ни у кого не вызывали возражений. Теперь мы стремимся данные инкапсулировать. Десять лет назад написание функций без учета влияния исключений было нормой. А сейчас мы боремся за достижение безопасности относительно исключений.</P> <P>Времена меняются. Мы живем. Мы учимся.</P> == Что следует помнить == *Безопасные относительно исключений функции не допускают утечки ресурсов и повреждения структур данных, даже в случае возбуждения исключений. Такие функции предоставляют базовую гарантию, строгую гарантию либо гарантию полного отсутствия исключений. *Строгая гарантия часто может быть реализована посредством копирования и обмена, но предоставлять ее для всех функций непрактично. *Функция обычно может предоставить гарантию не строже, чем самая слабая гарантия, обеспечиваемая вызываемыми из нее функциями. 74d436436a2febfc68b137784e7b7cf39eec80f7 61 60 2013-07-11T12:51:54Z Lerom 3360334 wikitext text/x-wiki <P>Безопасность исключений в чем-то подобна беременности… но пока отложим эту мысль в сторонку. Нельзя всерьез говорить о репродуктивной функции, пока не завершился этап ухаживания.</P> <P>Предположим, что у нас есть класс, представляющий меню с фоновыми картинками в графическом интерфейсе пользователя. Этот класс предназначен для использования в многопоточной среде, поэтому он включает мьютекс для синхронизации доступа:</P> <source lang="cpp"> class PrettyMenu { public: ... void changeBackground(std::istream& imgSrc); // сменить фоновую ... // картинку private: Mutex mutex; // мьютекс объекта Image *bgImage; // текущая фоновая картинка int imageChanges; // сколько раз картинка менялась }; </source> <P>Рассмотрим следующую возможную реализацию функции-члена change-Background:</P> <source lang="cpp"> void PrettyMenu::changeBackground(std::istreamS lmgSrc) { lock(Smutex); // захватить мьютекс delete bglmage; // избавиться от старой картинки ++imageChanges; // обновить счетчик изменений картинки bglmage = new Image(lmgSrc); // установить новый фон unlock(Smutex); // освободить мьютекс } </source> <P>С точки зрения безопасности исключений, эта функция настолько плоха, насколько вообще возможно. К безопасности исключений предъявляется два требования, и она не удовлетворяет ни одному из них.</P> <P>Когда возбуждается исключение, то безопасная относительно исключений функция:</P> *<STRONG>Не допускает утечки ресурсов.</STRONG> Приведенный код не проходит этот тест, потому что если выражение «new Image(imgSrc)» возбудит исключение, то вызов unlock никогда не выполнится, и мьютекс окажется захваченным навсегда. *<STRONG>Не допускает повреждения структур данных.</STRONG> Если «new Image(imgSrc)» возбудит исключение, в bgImage останется указатель на удаленный объект. Кроме того, счетчик imageChanges увеличивается, несмотря на то что новая картинка не установлена. (С другой стороны, старая картинка уже полностью удалена, так что трудно сделать вид, будто ничего не изменилось.) <P>Справиться с утечкой ресурсов легко – в [[Правило 13: Используйте объекты для управления ресурсами | правиле 13]] объяснено, как пользоваться объектами, управляющими ресурсами, а в [[Правило 14: Тщательно продумывайте поведение при копировании классов, управляющих ресурсами | правиле 14]] представлен класс Lock, гарантирующий своеременное освобождение мьютексов:</P> <source lang="cpp"> void PrettyMenu::changeBackground(std::istreamS lmgSrc) { Lock ml(mutex); // из правила 14: захватить мьютекс // и гарантировать его последующее освобождение delete bglmage; ++imageChanges; bglmage = new Image(lmgSrc); } </source> <P>Одним из преимуществ классов для управления ресурсами, подобных Lock, является то, что обычно они уменьшают размер функций. Заметили, что вызов unlock уже не нужен? Общее правило гласит: чем меньше кода, тем лучше, потому что меньше возможностей для ошибок и меньше путаницы при внесении изменений.</P> <P>От утечки ресурсов перейдем к проблеме возможного повреждения данных. Здесь у нас есть выбор, но прежде чем его сделать, нужно уточнить терминологию.</P> <P>Безопасные относительно исключений функции предоставляют одну из трех гарантий.</P> *Функции, предоставляющие <STRONG>базовую гарантию,</STRONG> обещают, что если исключение будет возбуждено, то все в программе остается в корректном состоянии. Никакие объекты или структуры данных не повреждены, и все объекты находятся в непротиворечивом состоянии (например, все инварианты классов не нарушены). Однако точное состояние программы может быть непредсказуемо. Например, мы можем написать функцию change-Background так, что при возникновении исключения объект PrettyMenu сохранит старую фоновую картинку либо у него будет какой-то фон по умолчанию, но пользователи не могут заранее знать, какой. (Чтобы выяснить это, им придется вызвать какую-то функцию-член, которая сообщит, какая сейчас используется картинка.) *Функции, предоставляющие <STRONG>строгую гарантию,</STRONG> обещают, что если исключение будет возбуждено, то состояние программы не изменится. Вызов такой функции является атомарным; если он завершился успешно, то все запланированные действия выполнены до конца, если же нет, то программа останется в таком состоянии, как будто функция никогда не вызывалась. Работать с функциями, представляющими такую гарантию, проще, чем с функциями, которые дают только базовую гарантию, потому что после их вызова может быть только два состояния программы: то, которое ожидается в результате ее успешного завершения, и то, которое было до ее вызова. Напротив, если исключение возникает в функции, представляющей только базовую гарантию, то программа может оказаться в <EM>любом</EM> корректном состоянии. *Функции, предоставляющие <STRONG>гарантию отсутствия исключений,</STRONG> обещают никогда не возбуждать исключений, потому что всегда делают то, что должны делать. Все операции над встроенными типами (например, целыми, указателями и т. п.) обеспечивают такую гарантию. Это основной строительный блок безопасного относительно исключений кода. <P>Разумно предположить, что функции с пустой спецификацией исключений не возбуждают их, но это не всегда так. Например, рассмотрим следующую функцию:</P> <source lang="cpp"> int doSomethmg () throw(); // обратите внимание на пустую // спецификацию исключений </source> <P>Это объявление не говорит о том, что doSomething никогда не возбуждает исключений. Утверждается лишь, что <EM>если</EM> doSomething возбудит исключение, значит, произошла серьезная ошибка и должна быть вызвана функция unexpected (более подробную информацию о функции unexpected вы можете найти, воспользовавшись поисковым сервисом или в полном руководстве по языку C++ (возможно, стоит поискать информацию о функции set_unexpected, которая специфицирует unexpected)). Фактически doSomething может вообще не представлять никаких гарантий относительно исключений. Объявление функции (включающее ее спецификацию исключений) ничего не сообщает относительно того, является ли она корректной, переносима, эффективной, какие гарантии безопасности исключений она предоставляет и предоставляет ли их вообще. Все эти характеристики определяются реализацией функции, а не ее объявлением.</P> <P>Безопасный относительно исключений код должен представлять одну из трех описанных гарантий. Если он этого не делает, он не является безопасным. Выбор, таким образом, в том, чтобы определить, какой тип гарантии должна представлять каждая из написанных вами функций. Если не считать унаследованный код, небезопасный относительно исключений (об этом мы поговорим далее в настоящем правиле), то отсутствие гарантий допустимо лишь, если в результате анализа требований было решено, что приложение просто обязано допускать утечку ресурсов и работать с поврежденными структурами данных.</P> <P>Вообще говоря, нужно стремиться предоставить максимально строгие гарантии. С точки зрения безопасности исключений функции, не возбуждающие исключений, чудесны, но очень трудно, не оставаясь в рамках языка C, обойтись без вызова функций, возбуждающих исключения. Любой класс, в котором используется динамическое распределение памяти (например, STL-контейнеры), может возбуждать исключение bad_alloc, когда не удается найти достаточного объема свободной памяти ([[Правило 49: Разберитесь в поведении обработчика new | см. правило 49]]). Предоставляйте гарантии отсутствия исключений, когда можете, но для большинства функций есть только выбор между базовой и строгой гарантией.</P> <P>Для функции changeBackground предоставить <EM>почти</EM> строгую гарантию нетрудно. Во-первых, измените тип данных bgImage в классе PrettyMenu со встроенного указателя *Image на один из «интеллектуальных» управляющих ресурсами указателей, описанных в [[Правило 13: Используйте объекты для управления ресурсами | правиле 13]]. Откровенно говоря, это в любом случае неплохо, поскольку позволяет избежать утечек ресурсов. Тот факт, что это заодно помогает обеспечить строгую гарантию безопасности исключений, просто подтверждает приведенные в [[Правило 13: Используйте объекты для управления ресурсами | правиле 13]] аргументы в пользу применения объектов (наподобие интеллектуальных указателей) для управления ресурсами. Ниже я воспользовался классом tr1::shared_ptr, потому что он ведет себя более естественно при копировании, чем auto_ptr.</P> <P>Во-вторых, нужно изменить порядок предложений в функции changeBackground так, чтобы значение счетчика imageChanges не увеличивалось до тех пор, пока картинка не будет заменена. Общее правило таково: помечайте в объекте, что произошло некоторое изменение, только после того, как это изменение действительно выполнено.</P> <P>Вот что получается в результате:</P> <source lang="cpp"> class PrettyMenu { ... std::trl::shared_ptr<Image> bglmage; ... void PrettyMenu::changeBackground(std::lstreamS lmgSrc) { Lock ml(mutex); Bglmage.reset(new Image(imgSrc)); // заменить внутренний указатель // bglmage результатом выражения "new Image" ++imageChanges; } </source> <P>Отметим, что больше нет необходимости вручную удалять старую картинку, потому что это делает «интеллектуальный» указатель. Более того, удаление происходит только в том случае, если новая картинка успешно создана. Точнее говоря, функция tr1::shared_ptr::reset будет вызвана, только в том случае, когда ее параметр (результат вычисления «new Image(imgSrc)») успешно создан. Оператор delete используется только внутри вызова reset, поэтому если функция не получает управления, то и delete не вызывается. Отметим также, что использование объекта (tr1::shared_ptr) для управления ресурсом (динамически выделенным объектом Image) ко всему прочему уменьшает размер функции changeBackground.</P> <P>Как я сказал, эти два изменения позволяют changeBackground предоставлять <EM>почти</EM> строгую гарантию безопасности исключений. Так чего же не хватает? Дело в параметре imgSrc. Если конструктор Image возбудит исключение, может случиться, что указатель чтения из входного потока сместится, и такое смещение может оказаться изменением состояния, видимым остальной части программы. До тех пор пока у функции changeBackground есть этот недостаток, она предоставляет только базовую гарантию безопасности исключений.</P> <P>Но оставим в стороне этот нюанс и будем считать, что changeBackground представляет строгую гарантию безопасности. (По секрету сообщу, что есть способ добиться этого, изменив тип параметра с istream на имя файла, содержащего данные картинки.) Существует общая стратегия проектирования, которая обеспечивает строгую гарантию, и важно ее знать. Стратегия называется «скопировать и обменять» (copy and swap). В принципе, это очень просто. Сделайте копию объекта, который собираетесь модифицировать, затем внесите все необходимые изменения в копию. Если любая из операций модификации возбудит исключение, исходный объект останется неизменным. Когда все изменения будут успешно внесены, обменяйте модифицированный объект с исходным с помощью операции, не возбуждающей исключений.</P> <P>Обычно это реализуется помещением всех имеющих отношение к объекту данных из «реального» объекта в отдельный внутренний объект, на который в «реальном» объекте имеется указатель. Часто этот прием называют «идиома pimpl», и [[Правило 31: Уменьшайте зависимости файлов при компиляции | в правиле 31]] он описывается более подробно. Для класса PrettyMenu это может выглядеть примерно так:</P> <source lang="cpp"> struct PMImpl { // PMImpl = “PrettyMenu Impl”: std::tr1::shared_ptr<Image> bgImage; // см. далее – почему это int imageChanges; // структура, а не класс } class PrettyMenu { ... private: Mutex mutex; std::tr1::shared_ptr<PMImpl> pimpl; }; void PrettyMenu::changeBackground(std::istream& imgSrc) { using std::swap; // см. правило 25 Lock ml(&mutex); // захватить мьютекс std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pimpl)); // копировать данные obj pNew->bgImage.reset(new Image(imgSrc)); // модифицировать копию ++pNew->imageChanges; swap(pimpl, pNew); // обменять значения } // освободить мьютекс </source> <P>В этом примере я решил сделать PMImpl структурой, а не классом, потому что инкапсуляция данных PrettyMenu достигается за счет того, что член pImpl объявлен закрытым. Объявить PMImpl классом было бы ничем не хуже, хотя и менее удобно (зато поборники «объектно-ориентированной чистоты» были бы довольны). Если нужно, PMImpl можно поместить внутрь PrettyMenu, но такое перемещение никак не влияет на написание безопасного относительно исключений кода.</P> <P>Стратегия копирования и обмена – это отличный способ внести изменения в состояние объекта по принципу «все или ничего», но в общем случае при этом не гарантируется, что вся функция в целом строго безопасна относительно исключений. Чтобы понять почему, абстрагируемся от функции changeBackground и рассмотрим вместо нее некоторую функцию someFunc, которая использует копирование с обменом, но еще и обращается к двум другим функциям: f1 и f2.</P> <source lang="cpp"> void someFunc() { ... // скопировать локальное состояние f1(); f2() ; ... // обменять модифицированное состояние с копией } </source> <P>Должно быть ясно, что если f1 или f2 не обеспечивают строгих гарантий безопасности исключений, то будет трудно обеспечить ее и для someFunc в целом. Например, предположим, что f1 обеспечивает только базовую гарантию. Чтобы someFunc обеспечивала строгую гарантию, необходимо написать код, определяющий состояние всей программы до вызова f1, перехватить все исключения, которые может возбудить f1, а затем восстановить исходное состояние.</P> <P>Ситуация не становится существенно лучше, если и f1, и f2 обеспечивают строгую гарантию безопасности исключений. Ведь если f1 нормально доработает до конца, состояние программы может измениться произвольным образом, поэтому если f2 возбудит исключение, то состояние программы не будет тем же, как перед вызовом someFunc, даже если f2 не изменит ничего.</P> <P>Проблема в побочных эффектах. До тех пор пока функция оперирует только локальным состоянием (то есть someFunc влияет только на состояние объекта, для которого вызвана), относительно легко обеспечить строгую гарантию. Но когда функция имеет побочные эффекты, затрагивающие нелокальные данные, все становится сложнее. Если, например, побочным эффектом вызова f1 является модификация базы данных, будет трудно обеспечить строгую гарантию для someFunc. Не существует способа отменить модификацию базы данных, которая уже была совершена: другие клиенты могли уже увидеть новое состояние.</P> <P>Подобные ситуации могут помешать предоставлению строгой гарантии безопасности для функции, даже если вы хотели бы это сделать. Кроме того, надо принять во внимание эффективность. Смысл «копирования и обмена» в том, чтобы модифицировать копию данных объекта, а затем обменять модифицированные и исходные данные операцией, которая не возбуждает исключений. Для этого нужно сделать копию каждого объекта, который подлежит модификации, что потребует времени и памяти, которыми вы, возможно, не располагаете. Строгая гарантия весьма желательна, и вы должны обеспечивать ее, когда это разумно и практично, но не обязательно во всех случаях.</P> <P>Когда невозможно предоставить строгую гарантию, вы должны обеспечить базовую. На практике может оказаться так, что для некоторых функций можно обеспечить строгую гарантию, тогда как для многих других это неразумно из соображений эффективности и сложности. Если вы сделали все возможное для обеспечения строгой гарантии там, где это оправдано, никто не вправе критиковать вас за то, что в остальных случаях вы представляете только базовую гарантию. Для многих функций базовая гарантия – совершенно разумный выбор.</P> <P>Совсем другое дело, если вы пишете функцию, которая вообще не представляет никаких гарантий безопасности исключений. Тут вступает в силу презумпция виновности: подсудимый считается виновным, пока не докажет обратного. Вы <EM>должны</EM> писать код, безопасный относительно исключений. Однако у вас есть право на защиту. Рассмотрим еще раз реализацию функции someFunc, которая вызывает f1 и f2. Предположим, что f2 не представляет никаких гарантий безопасности исключений, даже базовой. Это значит, что если f2 возбудит исключение, то возможна утечка ресурсов внутри f2. Это также означает, что f2 может повредить структуры данных, например отсортированные массивы могут стать неотсортированными, объект, который копировался из одной структуры в другую, может потеряться и т. д. Функция someFunc ничего не может с этим поделать. Если вызываемые из someFunc функции не гарантируют безопасности относительно исключений, то и someFunc не может предоставить никаких гарантий.</P> <P>Вот теперь мы можем вернуться к теме беременности. Женщина либо беременна, либо нет. Невозможно быть чуть-чуть беременной. Аналогично программная система является либо безопасной по исключениям, либо нет. Нет такого понятия, как частично безопасная система. Если система имеет всего одну небезопасную относительно исключений функцию, то она небезопасна и в целом, потому что вызов этой функции может привести к утечке ресурсов и повреждению структур данных. К несчастью, большинство унаследованного кода на C++ было написано без учета требований безопасности исключений, поэтому многие системы на сегодня являются в этом отношении небезопасными. Они включают код, написанный в небезопасной манере.</P> <P>Но нет причин сохранять такое положение дел навсегда. При написании нового кода или модификации существующего тщательно продумывайте способы достижения безопасности исключений. Начните с применения объектов управления ресурсами ([[Правило 13: Используйте объекты для управления ресурсами | см. правило 13]]). Это предотвратит утечку ресурсов. Затем определите, какую максимальную из трех гарантий безопасности исключений вы можете обеспечить для разрабатываемых функций, оставляя их небезопасными только в том случае, когда вызовы унаследованного кода не оставляют другого выбора. Документируйте ваши решения как для пользователей ваших функций, так и для сопровождения в будущем. Гарантия безопасности исключений функции – это видимая часть ее интерфейса, поэтому вы должны подходить к ней столь же ответственно, как и к другим аспектам интерфейса.</P> <P>Сорок лет назад код, изобилующий операторами goto, считался вполне приемлемым. Теперь же мы стараемся писать структурированные программы. Двенадцать лет назад глобальные данные ни у кого не вызывали возражений. Теперь мы стремимся данные инкапсулировать. Десять лет назад написание функций без учета влияния исключений было нормой. А сейчас мы боремся за достижение безопасности относительно исключений.</P> <P>Времена меняются. Мы живем. Мы учимся.</P> == Что следует помнить == *Безопасные относительно исключений функции не допускают утечки ресурсов и повреждения структур данных, даже в случае возбуждения исключений. Такие функции предоставляют базовую гарантию, строгую гарантию либо гарантию полного отсутствия исключений. *Строгая гарантия часто может быть реализована посредством копирования и обмена, но предоставлять ее для всех функций непрактично. *Функция обычно может предоставить гарантию не строже, чем самая слабая гарантия, обеспечиваемая вызываемыми из нее функциями. e6a29716e2986adcaa8e0293275a53574203015c Реализация паттерна««Шаблонный метод» с помощью идиомы невиртуального интерфейса 0 29 62 2013-08-22T09:54:51Z Lerom 3360334 Новая страница: «<P>Встроенные функции – какая <EM>замечательная</EM> идея! Они выглядят подобно функциям, он…» wikitext text/x-wiki <P>Встроенные функции – какая <EM>замечательная</EM> идея! Они выглядят подобно функциям, они работают подобно функциям, они намного лучше макросов ([[Правило 2: Предпочитайте const, enum и inline использованию #define | см. правило 2]]). Их можно вызывать, не опасаясь накладных расходов, связанных с вызовом обычных функций. Чего еще желать?</P> <P>В действительности вы получаете больше, чем рассчитывали, потому что возможность избежать затрат на вызов функции – это только полдела. Оптимизация, выполняемая компилятором, обычно наиболее эффективна на участке кода, не содержащем вызовов функций. Таким образом, вы даете компилятору возможность оптимизации тела встроенной функции в зависимости от объемлющего контекста. При использовании «обычного» функционального вызова большинство компиляторов такой оптимизации на обычных не выполняют.</P> <P>Все же давайте не будем слишком увлекаться. В программировании, как и в реальной жизни, не бывает «бесплатных завтраков», и встроенные функции – не исключение. Идея их использования состоит в замене каждого вызова такой функции ее телом. Не нужно быть доктором математических наук, чтобы заметить, что это увеличит общий размер вашего объектного кода. Слишком частое применение встроенных функций на машинах с ограниченной памятью может привести к созданию программы, которая превосходит доступную память. Даже при наличии виртуальной памяти «разбухание» кода, вызванное применением встроенных функций, может привести к дополнительному обмену с диском, уменьшить коэффициент попадания команд в кэш и, следовательно, снизить производительность программы.</P> <P>С другой стороны, если тело встроенной функции <EM>очень</EM> короткое, то сгенерированный для нее код может быть короче кода, сгенерированного для вызова функции. В таком случае встраивание функции может привести к <EM>уменьшению</EM> объектного кода и повышению коэффициента попаданий в кэш!</P> <P>Имейте в виду, что директива inline – это <EM>совет,</EM> а не команда компилятору. Совет может быть сформулирован явно или неявно. Неявный способ заключается в определении встроенной функции внутри определения класса:</P> <source lang="cpp"> class Person { public: ... int age() const { return theAge;} // неявный запрос на встраивание; ... // функция age определена внутри класса private: int theAge; }; </source> <P>Такие функции обычно являются функциями-членами, но в [[Правило 46: Определяйте внутри шаблонов функции, не являющиеся членами, когда желательны преобразования типа | правиле 46]] объясняется, что функции-друзья тоже могут быть определены внутри класса. В этом случае они также неявно считаются встроенными.</P> <P>Явно объявить встроенную функцию можно, предварив ее определение ключевым словом inline. Например, вот как обычно реализован стандартный шаблон max (из заголовочного файла &lt;algorithm&gt;):</P> <source lang="cpp"> template <typename T> // явный запрос на inline const T& std::max(const T& a, const T& b) // встраивание: функции { return a < b ? b : c;} // std::max предшествует // слово inline </source> <P>Тот факт, что max – это шаблон, наводит на мысль, что встроенные функции и шаблоны обычно объявляются в заголовочных файлах. Некоторые программисты делают из этого вывод, что шаблоны функций обязательно должны быть встроенными. Это заключение одновременно неверно и потенциально опасно, поэтому рассмотрим его внимательнее.</P> <P>Встроенные функции обычно должны находиться в заголовочных файлах, поскольку большинство разработки программ выполняют встраивание во время компиляции. Чтобы заменить вызовы функции встраиванием ее тела, компилятор должен увидеть эту функцию. (Некоторые среды могут встраивать функции во время компоновки, а есть и такие – например, среды разработки на базе. NET Common Language Infrastructure (CLI), – которые осуществляют встраивание во время исполнения. Но это скорее исключение, чем правило. Встраивание функций в большинстве программ на C++ происходит во время компиляции.)</P> <P>Шаблоны обычно находятся в заголовочных файлах, потому что компилятор должен знать, как шаблон выглядит, чтобы конкретизировать его в момент использования. (Но и это правило не является универсальным. Некоторые среды разработки выполняют конкретизацию шаблонов во время компоновки. Однако конкретизация на этапе компиляции встречается чаще.)</P> <P>Конкретизация шаблонов никак не связана со встраиванием. Если вы полагаете, что все функции, конкретизированные из вашего шаблона, должны быть встроенными, объявите шаблон встроенным (inline); именно так разработчики стандартной библиотеки поступили с шаблоном std::max (см. пример выше). Но если вы пишете шаблон для функции, которую нет смысла делать встроенной, не объявляйте встроенным и ее шаблон (явно или неявно). Встраивание обходится дорого, и вряд ли вы захотите платить за это без должного размышления. Мы уже упоминали, что встраивание раздувает код (особенно это важно при разработке шаблонов – [[Правило 44: Размещайте независимый от параметров код вне шаблонов | см. правило 44]]), но есть и другие затраты, которые мы скоро обсудим.</P> <P>Но прежде напомним, что встраивание – это совет, который компилятор может проигнорировать. Большинство компиляторов отвергают встраивание функций, которые представляются слишком сложными (например, содержат циклы или рекурсию), и за исключением наиболее тривиальных случаев, вызов виртуальной функции отменяет встраивание. В этом нет ничего удивительного: virtual означает «какую точно функцию вызвать, определяется в момент исполнения», а inline – «перед исполнением заменить вызов функции ее кодом». Если компилятор не знает, какую функцию вызывать, то трудно винить его в том, что он отказывается делать встраивание.</P> <P>Все это в конечном счете сводится к следующему: от реализации используемого компилятора зависит, встраивается ли в действительность встроенная функция. К счастью, большинство компиляторов обладают достаточными диагностическими возможностями и выдают предупреждение ([[Правило 53: Обращайте внимание на предупреждения компилятора | см. правило 53]]), если не могут выполнить запрошенное вами встраивание.</P> <P>Иногда компилятор генерирует тела встроенной функции, даже если ничто не мешает ее встроить. Например, если ваша программа получает адрес встроенной функции, то компилятор, как правило, должен сгенерировать настоящее тело функции. Как иначе он может получить адрес функции, если ее не существует? В совокупности с тем фактом, что обычно компиляторы не выполняют встраивание, если функция вызывается по указателю, это значит, что вызовы встроенных функций могут встраиваться или не встраиваться в зависимости от того, как к ней производится обращение:</P> <source lang="cpp"> inline void f() {...} // предположим, что компилятор может встроить вызовы f void (*pf)() = f; // pf указывает на f ... f(); // этот вызов будет встроенным, потому что он // «нормальный» pf(); // этот вызов, вероятно, не будет встроен, потому что // функция вызвана по указателю </source> <P>Призрак невстраиваемых inline-функций может преследовать вас, даже если вы никогда не используете указателей на функции, потому что указатели на функции может запрашивать не только программист. Иногда компилятор генерирует невстраиваемые копии конструкторов и деструкторов так, что они запрашивают указатели на функции во время конструирования и разрушения объектов в массивах.</P> <P>Фактически конструкторы и деструкторы часто являются наихудшими кандидатами для встраивания. Например, рассмотрим конструктор класса Derived:</P> <source lang="cpp"> class Base { public: ... private: std::string bm1, bm2; // члены базового класса 1 и 2 }; class Derived: public Base { public: Derived(){} // конструктор Derived пуст – не так ли? ... private: std::string dm1, dm2, dm3; // члены производного класса 1–3 }; </source> <P>Этот конструктор выглядит как отличный кандидат на встраивание, поскольку он не содержит никакого кода. Но впечатление обманчиво.</P> <P>C++ дает различные гарантии о том, что должно происходить при конструировании и разрушении объектов. Например, когда вы используете оператор new, динамически создаваемые объекты автоматически инициализируются своими конструкторами, а при обращении к delete вызываются соответствующие деструкторы. Когда вы создаете объект, то автоматически конструируются члены всех его базовых классов, а равно его собственные данные-члены, а во время удаления объекта автоматически происходит обратный процесс. Если во время конструирования объекта возбуждается исключение, то все части объекта, которые были к этому моменту сконструированы, автоматически разрушаются. Во всех этих случаях C++ говорит, <EM>что</EM> должно случиться, но не говорит – <EM>как.</EM> Это зависит от реализации компилятора, но должно быть понятно, что такие вещи не происходят сами по себе. В вашей программе должен быть какой-то код, который все это реализует, и этот код, который генерируется компилятором и вставляется в вашу программу, должен где-то находиться. Иногда он помещается в конструкторы и деструкторы, поэтому можем представить себе следующую реализацию сгенерированного кода в якобы пустом конструкторе класса Derived:</P> <source lang="cpp"> Derived::Derived() // концептуальная реализация { // «пустого» конструктора класса Derived Base::Base(); // инициализировать часть Base try {dm1.std::string::string();} // попытка сконструировать dm1 catch(…) { // если возбуждается исключение, Base::~Base(); // разрушить часть базового класса throw; // распространить исключение выше } try {dm2.std::string::string();} // попытка сконструировать dm2 catch(…){ // если возбуждается исключение, dm1.std::string::~string(); // разрушить dm1 Base::~Base(); // разрушить часть базового класса throw; // распространить исключение } try {dm3.std::string::string();} // сконструировать dm3 catch(…){ // если возбуждается исключение, dm2.std::string::~string(); // разрушить dm2 dm1.std::string::~string(); // разрушить dm1 Base::~Base(); // разрушить часть базового класса throw; // распространить исключение } } </source> <P>В действительности это не совсем тот код, который порождает компилятор, потому что реальные компиляторы обрабатывают исключения более сложным образом. И все же этот пример довольно точно отражает поведение «пустого» конструктора класса Derived. Независимо от того, насколько хитроумно обходится с исключениями компилятор, конструктор Derived должен, по крайней мере, вызывать конструкторы своих данных-членов и базового класса, и эти вызовы (которые сами по себе могут быть встроенными) могут свести преимущества встраивания на нет.</P> <P>То же самое относится и к конструктору класса Base, поэтому если он встроенный, то весь вставленный в него код вставляется также и в конструктор Derived (поскольку конструктор Derived вызывает конструктор Base). И если конструктор класса string тоже окажется встроенным, то в конструктор Derived его код войдет <EM>пять</EM> раз – по одному для каждой из пяти имеющихся в классе Derived строк (две унаследованные и три, объявленные в нем самом). Наверное, теперь вам ясно, почему решений о встраивании конструктора Derived не стоит принимать с легким сердцем. Аналогично обстоят дела и с деструктором класса Derived, который каким-то образом должен гарантировать правильное уничтожение всех объектов, инициализированных конструктором.</P> <P>Разработчики библиотек должны принимать во внимание, что произойдет при объявлении функций встроенными, потому что невозможно предоставить двоичное обновление видимых клиенту встроенных библиотечных функций. Другими словами, если f – встроенная библиотечная функция, то пользователи этой библиотеки встраивают ее тело в свои приложения. Если разработчик библиотеки позднее решит изменить f, то все программы, которые ее использовали, придется откомпилировать заново. Часто это нежелательно. С другой стороны, если f не будет встроенной функцией, то после ее модификации клиентские программы нужно будет лишь заново компоновать с библиотекой. Это ощутимо быстрее, чем перекомпиляция, а если библиотека, содержащая функцию, является динамической, то изменения в ней вообще будут прозрачны для пользователей.</P> <P>При разработке программ важно иметь в виду все эти соображения, но с практической точки зрения наиболее существен следующий факт: у большинства отладчиков возникают проблемы со встроенными функциями. Это совсем не удивительно. Как установить точку остановки в функции, которой не существует? Хотя некоторые среды разработки ухитряются поддерживать отладку встроенных функций, во многих встраивание для отладочных версий просто отключается.</P> <P>Это приводит нас к следующей стратегии выбора функций, подходящих для встраивания. Поначалу откажитесь от встроенных функций вовсе, или, по крайней мере, ограничьтесь теми, которые обязаны быть встроенными ([[Правило 46: Определяйте внутри шаблонов функции, не являющиеся членами, когда желательны преобразования типа | см. правило 46]]) либо являются тривиальными (такие как Person::age выше). Применяя встроенные функции с должной аккуратностью, вы не только получаете возможность пользоваться отладчиком, но и определяете встраиванию подобающее место: тонкая оптимизация вручную. Не забывайте об эмпирическом правиле «80–20», которое утверждает, что типичная программа тратит 80 % времени на исполнение 20 % кода. Это важное правило, поскольку оно напоминает, что цель разработчика программного обеспечения – идентифицировать те 20 % кода, которые действительно способны повысить производительность программы. Можно до бесконечности оптимизировать и объявлять функции inline, но все это будет пустой тратой времени, если только вы не сосредоточите усилия на <EM>нужных</EM> функциях.</P> == Что следует помнить == *Делайте встраиваемыми только небольшие, часто вызываемые функции. Это облегчит отладку, даст возможность выполнять обновления библиотек на двоичном уровне, уменьшит эффект «разбухания» кода и поможет повысить быстродействие программы. *Не объявляйте шаблоны функций встроенными только потому, что они появляются в заголовочных файлах. 398db3dafc4662b79bba3b7e00f66b66336ac303 64 62 2013-08-22T09:55:55Z Lerom 3360334 Полностью удалено содержимое страницы wikitext text/x-wiki da39a3ee5e6b4b0d3255bfef95601890afd80709 Правило 30: Тщательно обдумывайте использование встроенных функций 0 30 63 2013-08-22T09:55:49Z Lerom 3360334 Новая страница: «<P>Встроенные функции – какая <EM>замечательная</EM> идея! Они выглядят подобно функциям, он…» wikitext text/x-wiki <P>Встроенные функции – какая <EM>замечательная</EM> идея! Они выглядят подобно функциям, они работают подобно функциям, они намного лучше макросов ([[Правило 2: Предпочитайте const, enum и inline использованию #define | см. правило 2]]). Их можно вызывать, не опасаясь накладных расходов, связанных с вызовом обычных функций. Чего еще желать?</P> <P>В действительности вы получаете больше, чем рассчитывали, потому что возможность избежать затрат на вызов функции – это только полдела. Оптимизация, выполняемая компилятором, обычно наиболее эффективна на участке кода, не содержащем вызовов функций. Таким образом, вы даете компилятору возможность оптимизации тела встроенной функции в зависимости от объемлющего контекста. При использовании «обычного» функционального вызова большинство компиляторов такой оптимизации на обычных не выполняют.</P> <P>Все же давайте не будем слишком увлекаться. В программировании, как и в реальной жизни, не бывает «бесплатных завтраков», и встроенные функции – не исключение. Идея их использования состоит в замене каждого вызова такой функции ее телом. Не нужно быть доктором математических наук, чтобы заметить, что это увеличит общий размер вашего объектного кода. Слишком частое применение встроенных функций на машинах с ограниченной памятью может привести к созданию программы, которая превосходит доступную память. Даже при наличии виртуальной памяти «разбухание» кода, вызванное применением встроенных функций, может привести к дополнительному обмену с диском, уменьшить коэффициент попадания команд в кэш и, следовательно, снизить производительность программы.</P> <P>С другой стороны, если тело встроенной функции <EM>очень</EM> короткое, то сгенерированный для нее код может быть короче кода, сгенерированного для вызова функции. В таком случае встраивание функции может привести к <EM>уменьшению</EM> объектного кода и повышению коэффициента попаданий в кэш!</P> <P>Имейте в виду, что директива inline – это <EM>совет,</EM> а не команда компилятору. Совет может быть сформулирован явно или неявно. Неявный способ заключается в определении встроенной функции внутри определения класса:</P> <source lang="cpp"> class Person { public: ... int age() const { return theAge;} // неявный запрос на встраивание; ... // функция age определена внутри класса private: int theAge; }; </source> <P>Такие функции обычно являются функциями-членами, но в [[Правило 46: Определяйте внутри шаблонов функции, не являющиеся членами, когда желательны преобразования типа | правиле 46]] объясняется, что функции-друзья тоже могут быть определены внутри класса. В этом случае они также неявно считаются встроенными.</P> <P>Явно объявить встроенную функцию можно, предварив ее определение ключевым словом inline. Например, вот как обычно реализован стандартный шаблон max (из заголовочного файла &lt;algorithm&gt;):</P> <source lang="cpp"> template <typename T> // явный запрос на inline const T& std::max(const T& a, const T& b) // встраивание: функции { return a < b ? b : c;} // std::max предшествует // слово inline </source> <P>Тот факт, что max – это шаблон, наводит на мысль, что встроенные функции и шаблоны обычно объявляются в заголовочных файлах. Некоторые программисты делают из этого вывод, что шаблоны функций обязательно должны быть встроенными. Это заключение одновременно неверно и потенциально опасно, поэтому рассмотрим его внимательнее.</P> <P>Встроенные функции обычно должны находиться в заголовочных файлах, поскольку большинство разработки программ выполняют встраивание во время компиляции. Чтобы заменить вызовы функции встраиванием ее тела, компилятор должен увидеть эту функцию. (Некоторые среды могут встраивать функции во время компоновки, а есть и такие – например, среды разработки на базе. NET Common Language Infrastructure (CLI), – которые осуществляют встраивание во время исполнения. Но это скорее исключение, чем правило. Встраивание функций в большинстве программ на C++ происходит во время компиляции.)</P> <P>Шаблоны обычно находятся в заголовочных файлах, потому что компилятор должен знать, как шаблон выглядит, чтобы конкретизировать его в момент использования. (Но и это правило не является универсальным. Некоторые среды разработки выполняют конкретизацию шаблонов во время компоновки. Однако конкретизация на этапе компиляции встречается чаще.)</P> <P>Конкретизация шаблонов никак не связана со встраиванием. Если вы полагаете, что все функции, конкретизированные из вашего шаблона, должны быть встроенными, объявите шаблон встроенным (inline); именно так разработчики стандартной библиотеки поступили с шаблоном std::max (см. пример выше). Но если вы пишете шаблон для функции, которую нет смысла делать встроенной, не объявляйте встроенным и ее шаблон (явно или неявно). Встраивание обходится дорого, и вряд ли вы захотите платить за это без должного размышления. Мы уже упоминали, что встраивание раздувает код (особенно это важно при разработке шаблонов – [[Правило 44: Размещайте независимый от параметров код вне шаблонов | см. правило 44]]), но есть и другие затраты, которые мы скоро обсудим.</P> <P>Но прежде напомним, что встраивание – это совет, который компилятор может проигнорировать. Большинство компиляторов отвергают встраивание функций, которые представляются слишком сложными (например, содержат циклы или рекурсию), и за исключением наиболее тривиальных случаев, вызов виртуальной функции отменяет встраивание. В этом нет ничего удивительного: virtual означает «какую точно функцию вызвать, определяется в момент исполнения», а inline – «перед исполнением заменить вызов функции ее кодом». Если компилятор не знает, какую функцию вызывать, то трудно винить его в том, что он отказывается делать встраивание.</P> <P>Все это в конечном счете сводится к следующему: от реализации используемого компилятора зависит, встраивается ли в действительность встроенная функция. К счастью, большинство компиляторов обладают достаточными диагностическими возможностями и выдают предупреждение ([[Правило 53: Обращайте внимание на предупреждения компилятора | см. правило 53]]), если не могут выполнить запрошенное вами встраивание.</P> <P>Иногда компилятор генерирует тела встроенной функции, даже если ничто не мешает ее встроить. Например, если ваша программа получает адрес встроенной функции, то компилятор, как правило, должен сгенерировать настоящее тело функции. Как иначе он может получить адрес функции, если ее не существует? В совокупности с тем фактом, что обычно компиляторы не выполняют встраивание, если функция вызывается по указателю, это значит, что вызовы встроенных функций могут встраиваться или не встраиваться в зависимости от того, как к ней производится обращение:</P> <source lang="cpp"> inline void f() {...} // предположим, что компилятор может встроить вызовы f void (*pf)() = f; // pf указывает на f ... f(); // этот вызов будет встроенным, потому что он // «нормальный» pf(); // этот вызов, вероятно, не будет встроен, потому что // функция вызвана по указателю </source> <P>Призрак невстраиваемых inline-функций может преследовать вас, даже если вы никогда не используете указателей на функции, потому что указатели на функции может запрашивать не только программист. Иногда компилятор генерирует невстраиваемые копии конструкторов и деструкторов так, что они запрашивают указатели на функции во время конструирования и разрушения объектов в массивах.</P> <P>Фактически конструкторы и деструкторы часто являются наихудшими кандидатами для встраивания. Например, рассмотрим конструктор класса Derived:</P> <source lang="cpp"> class Base { public: ... private: std::string bm1, bm2; // члены базового класса 1 и 2 }; class Derived: public Base { public: Derived(){} // конструктор Derived пуст – не так ли? ... private: std::string dm1, dm2, dm3; // члены производного класса 1–3 }; </source> <P>Этот конструктор выглядит как отличный кандидат на встраивание, поскольку он не содержит никакого кода. Но впечатление обманчиво.</P> <P>C++ дает различные гарантии о том, что должно происходить при конструировании и разрушении объектов. Например, когда вы используете оператор new, динамически создаваемые объекты автоматически инициализируются своими конструкторами, а при обращении к delete вызываются соответствующие деструкторы. Когда вы создаете объект, то автоматически конструируются члены всех его базовых классов, а равно его собственные данные-члены, а во время удаления объекта автоматически происходит обратный процесс. Если во время конструирования объекта возбуждается исключение, то все части объекта, которые были к этому моменту сконструированы, автоматически разрушаются. Во всех этих случаях C++ говорит, <EM>что</EM> должно случиться, но не говорит – <EM>как.</EM> Это зависит от реализации компилятора, но должно быть понятно, что такие вещи не происходят сами по себе. В вашей программе должен быть какой-то код, который все это реализует, и этот код, который генерируется компилятором и вставляется в вашу программу, должен где-то находиться. Иногда он помещается в конструкторы и деструкторы, поэтому можем представить себе следующую реализацию сгенерированного кода в якобы пустом конструкторе класса Derived:</P> <source lang="cpp"> Derived::Derived() // концептуальная реализация { // «пустого» конструктора класса Derived Base::Base(); // инициализировать часть Base try {dm1.std::string::string();} // попытка сконструировать dm1 catch(…) { // если возбуждается исключение, Base::~Base(); // разрушить часть базового класса throw; // распространить исключение выше } try {dm2.std::string::string();} // попытка сконструировать dm2 catch(…){ // если возбуждается исключение, dm1.std::string::~string(); // разрушить dm1 Base::~Base(); // разрушить часть базового класса throw; // распространить исключение } try {dm3.std::string::string();} // сконструировать dm3 catch(…){ // если возбуждается исключение, dm2.std::string::~string(); // разрушить dm2 dm1.std::string::~string(); // разрушить dm1 Base::~Base(); // разрушить часть базового класса throw; // распространить исключение } } </source> <P>В действительности это не совсем тот код, который порождает компилятор, потому что реальные компиляторы обрабатывают исключения более сложным образом. И все же этот пример довольно точно отражает поведение «пустого» конструктора класса Derived. Независимо от того, насколько хитроумно обходится с исключениями компилятор, конструктор Derived должен, по крайней мере, вызывать конструкторы своих данных-членов и базового класса, и эти вызовы (которые сами по себе могут быть встроенными) могут свести преимущества встраивания на нет.</P> <P>То же самое относится и к конструктору класса Base, поэтому если он встроенный, то весь вставленный в него код вставляется также и в конструктор Derived (поскольку конструктор Derived вызывает конструктор Base). И если конструктор класса string тоже окажется встроенным, то в конструктор Derived его код войдет <EM>пять</EM> раз – по одному для каждой из пяти имеющихся в классе Derived строк (две унаследованные и три, объявленные в нем самом). Наверное, теперь вам ясно, почему решений о встраивании конструктора Derived не стоит принимать с легким сердцем. Аналогично обстоят дела и с деструктором класса Derived, который каким-то образом должен гарантировать правильное уничтожение всех объектов, инициализированных конструктором.</P> <P>Разработчики библиотек должны принимать во внимание, что произойдет при объявлении функций встроенными, потому что невозможно предоставить двоичное обновление видимых клиенту встроенных библиотечных функций. Другими словами, если f – встроенная библиотечная функция, то пользователи этой библиотеки встраивают ее тело в свои приложения. Если разработчик библиотеки позднее решит изменить f, то все программы, которые ее использовали, придется откомпилировать заново. Часто это нежелательно. С другой стороны, если f не будет встроенной функцией, то после ее модификации клиентские программы нужно будет лишь заново компоновать с библиотекой. Это ощутимо быстрее, чем перекомпиляция, а если библиотека, содержащая функцию, является динамической, то изменения в ней вообще будут прозрачны для пользователей.</P> <P>При разработке программ важно иметь в виду все эти соображения, но с практической точки зрения наиболее существен следующий факт: у большинства отладчиков возникают проблемы со встроенными функциями. Это совсем не удивительно. Как установить точку остановки в функции, которой не существует? Хотя некоторые среды разработки ухитряются поддерживать отладку встроенных функций, во многих встраивание для отладочных версий просто отключается.</P> <P>Это приводит нас к следующей стратегии выбора функций, подходящих для встраивания. Поначалу откажитесь от встроенных функций вовсе, или, по крайней мере, ограничьтесь теми, которые обязаны быть встроенными ([[Правило 46: Определяйте внутри шаблонов функции, не являющиеся членами, когда желательны преобразования типа | см. правило 46]]) либо являются тривиальными (такие как Person::age выше). Применяя встроенные функции с должной аккуратностью, вы не только получаете возможность пользоваться отладчиком, но и определяете встраиванию подобающее место: тонкая оптимизация вручную. Не забывайте об эмпирическом правиле «80–20», которое утверждает, что типичная программа тратит 80 % времени на исполнение 20 % кода. Это важное правило, поскольку оно напоминает, что цель разработчика программного обеспечения – идентифицировать те 20 % кода, которые действительно способны повысить производительность программы. Можно до бесконечности оптимизировать и объявлять функции inline, но все это будет пустой тратой времени, если только вы не сосредоточите усилия на <EM>нужных</EM> функциях.</P> == Что следует помнить == *Делайте встраиваемыми только небольшие, часто вызываемые функции. Это облегчит отладку, даст возможность выполнять обновления библиотек на двоичном уровне, уменьшит эффект «разбухания» кода и поможет повысить быстродействие программы. *Не объявляйте шаблоны функций встроенными только потому, что они появляются в заголовочных файлах. 398db3dafc4662b79bba3b7e00f66b66336ac303 Правило 31: Уменьшайте зависимости файлов при компиляции 0 31 65 2013-08-22T11:02:56Z Lerom 3360334 Новая страница: «<P>Рассмотрим самую обыкновенную ситуацию. Вы открываете свою программу на C++ и вносите н…» wikitext text/x-wiki <P>Рассмотрим самую обыкновенную ситуацию. Вы открываете свою программу на C++ и вносите незначительные изменения в реализацию класса. Заметьте, не в интерфейс класса, а просто в реализацию – только в закрытые члены. После этого вы начинаете заново собирать программу, рассчитывая, что это займет лишь несколько секунд. В конце концов, ведь вы модифицировали всего один класс. Вы щелкаете по кнопке Build или набираете make (либо какой-то эквивалент), и… удивлены, а затем – подавлены, когда обнаруживаете, что перекомпилируется и заново компонуется весь <EM>мир!</EM> Не правда ли, вам это скоро надоест?</P> <P>Проблема связана с тем, что C++ не проводит сколько-нибудь значительного различия между интерфейсом и реализацией. В частности, определения классов включают в себя не только спецификацию интерфейса, но также и целый ряд деталей реализации. Например:</P> <source lang="cpp"> class Person { public: Person(const std::string& name, const Date& birthday, const Address& addr); std::string name() const; std::string birthDate() const; std::string address() const; ... private: std::string theName; // деталь реализации Date theBirthDate; // деталь реализации Address theAddress; // деталь реализации }; </source> <P>Класс Person нельзя скомпилировать, не имея доступа к определению классов, с помощью которых он реализуется, а именно string, Date и Address. Такие определения обычно предоставляются посредством директивы #include, поэтому весьма вероятно, что в начале файла, определяющего класс Person, вы найдете нечто вроде:</P> <source lang="cpp"> #include <string> #include “date.h” #include “address.h” </source> <P>К сожалению, это устанавливает зависимости времени компиляции между файлом определения Person и включаемыми файлами. Если изменится любой из этих файлов либо любой из файлов, от которых <EM>они</EM> зависят, то должен быть перекомпилирован файл, содержащий определение Person, а равно и все файлы, которые класс Person используют. Такие каскадные зависимости могут быть весьма обременительны для пользователей.</P> <P>Можно задаться вопросом, почему C++ настаивает на размещении деталей реализации класса в определении класса. Например, почему нельзя определить Person следующим образом:</P> <source lang="cpp"> namespace std { class string; // опережающее объявление } // (некорректно – см. далее) class Date; // опережающее объявление class Address; // опережающее объявление class Person { public: Person(const std::string& name, const Date& birthday, const Address& addr); std::string name() const; std::string birthDate() const; std::string address() const; ... }; </source> <P>Если бы такое было возможно, то пользователи класса Person должны были перекомпилировать свои программы только при изменении его интерфейса.</P> <P>Увы, при реализации этой идеи мы наталкиваемся на две проблемы. Первая: string – это не класс, а typedef (синоним шаблона basic_string&lt;char&gt;). Поэтому опережающее объявление string некорректно. Правильное объявление гораздо сложнее, так как в нем участвуют дополнительные шаблоны. Впрочем, это не важно, потому что вы в любом случае не должны вручную объявлять какие-либо части стандартной библиотеки. Вместо этого просто включите с помощью #include правильные заголовки и успокойтесь. Стандартные заголовки вряд ли станут узким местом при компиляции, особенно если ваша среда разработки поддерживает предкомпилированные заголовочные файлы. Если на компиляцию стандартных заголовков все же уходит много времени, то может понадобиться изменить дизайн и избежать использования тех частей стандартной библиотеки, которые включать нежелательно.</P> <P>Вторая (и более существенная) неприятность, связанная с опережающим объявлением, состоит в том, что компилятору необходимо знать размер объектов во время компиляции. Рассмотрим пример:</P> <source lang="cpp"> int main() { int x; // определяем int Person p(params); // определяем Person ... } </source> <P>Когда компилятор видит определение x, он понимает, что должен выделить достаточно места (обычно в стеке) для размещения int. Нет проблем: каждый компилятор знает, какова длина int. Встречая определение p, компилятор учитывает, что нужно выделить место для Person, но откуда ему знать, сколько именно места потребуется? Единственный способ получить эту информацию – справиться в определении класса, но если бы в определениях классов можно было опускать детали реализации, как компилятор выяснил бы, сколько памяти необходимо выделить?</P> <P>Такой вопрос не возникает в языках типа SmallTalk или Java, потому что при определении объекта компиляторы выделяют только память, достаточную для хранения <EM>указателя</EM> на этот объект. Иначе говоря, эти языки интерпретируют вышеприведенный код, как если бы он был написан следующим образом:</P> <source lang="cpp"> int main() int main() { int x; // определяем int Person *p; // определяем указатель на Person ... } </source> <P>Это вполне законная конструкция на C++, поэтому вы и сами сможете имитировать «сокрытие реализации объекта за указателем». В случае класса Person это можно сделать, например, разделив его на два класса: один – для представления интерфейса, а другой – для его реализации. Если класс, содержащий реализацию, назвать Personlmpl, то Person должен быть написан следующим образом:</P> <source lang="cpp"> #include <string> // компоненты стандартной библиотеки // не могут быть объявлены предварительно #include <memory> // для tr1::shared_ptr; см. далее class PersonImpl; // опережающее объявление PersonImpl class Date; // опережающее объявление классов, class Address; // используемых в интерфейсе Person class Person { public: Person(const std::string& name, const Date& birthday, const Address& addr); std::string name() const; std::string birthDate() const; std::string address() const; ... private: // указатель на реализацию: std::tr1::shared_ptr<PersonImpl> pImpl; // см. в правиле 13 информацию }; // о std::tr1::shared_ptr </source> <P>Здесь главный класс (Person) не содержит никаких данных-членов, кроме указателя (в данном случае tr1::shared_ptr – см. правило 13) на свой класс реализации (Personlmpl). Такой дизайн часто называют «идиомой pimpl» («pointer to implementation» – указатель на реализацию). В подобных классах указатели часто называют pImpl, как в приведенном примере.</P> <P>При таком дизайне пользователи класса Person не видят никаких деталей – дат, адресов и имен. Реализация может быть модифицирована как угодно, при этом перекомпилировать программы, в которых используется Person, не придется. Кроме того, поскольку пользователи не знают деталей реализации Person, они вряд ли напишут код, который каким-то образом будет зависеть от этих деталей. Вот это я и называю отделением интерфейса от реализации.</P> <P>Ключом к этому разделению служит замена зависимости от <EM>определения</EM> (definition) на зависимость от <EM>объявления</EM> (declaration). Это и есть сущность минимизации зависимостей на этапе компиляции: когда это целесообразно, делайте заголовочные файлы самодостаточными; в противном случае используйте зависимость от объявлений, а не от определений. Все остальное вытекает из только что изложенной стратегии проектирования. Сформулируем три практических следствия:</P> <P>• <STRONG>Избегайте использования объектов, если есть шанс обойтись ссылками или указателями.</STRONG> Вы можете определить ссылки и указатели, имея только <EM>объявление</EM> типа. Определение <EM>объектов</EM> требует наличия <EM>определения</EM> типа.</P> <P>• <STRONG>По возможности используйте зависимость от объявления, а не от определения класса.</STRONG> Отметим, что для объявления функции, использующей некоторый класс, <EM>никогда</EM> не требуется определение этого класса, даже если функция принимает или возвращает объект класса по значению:</P> <source lang="cpp"> class Date; // объявление класса Date today(); // правильно, необходимость void clearAppointments(Date d); // в определении Date отсутствует </source> <P>Конечно, передача по значению – не очень хорошая идея ([[Правило 20: Предпочитайте передачу по ссылке на const передаче по значению | см. правило 20]]), но если по той или иной причине вы будете вынуждены ею воспользоваться, это никак не оправдает введения ненужных зависимостей. Не исключено, что возможность объявить функции today и clearAppoinments без определения Date повергла вас в удивление, но на самом деле это не так уж странно. Определение Date должно быть доступно в момент вызова этих функций. Да, я знаю, о чем вы думаете: зачем объявлять функции, которых никто не вызывает? Ответ прост. Дело не в том, что <EM>никто</EM> не вызывает их, а в том, что их вызывают <EM>не все.</EM> Например, если имеется библиотека, содержащая десятки объявлений функций, то маловероятно, что каждый пользователь вызывает каждую функцию. Перенося бремя ответственности за предоставление определений класса с ваших заголовочных файлов, содержащих <EM>объявления</EM> функций, на пользовательские файлы, содержащие их вызовы, вы исключаете искусственную зависимость пользователя от определений типов, которые им в действительности не нужны.</P> <P>• <STRONG>Размещайте объявления и определения в разных заголовочных файлах. </STRONG>Чтобы было проще придерживаться описанных выше принципов, файлы заголовков должны поставляться парами: один – для объявлений, второй – для определений. Конечно, нужно, чтобы эти файлы были согласованы. Если объявление изменяется в одном месте, то нужно изменить его и во втором. В результате пользователи библиотеки всегда должны включать файл объявлений, а не писать самостоятельно опережающие объявления, тогда как авторы библиотек должны поставлять оба заголовочных файла.</P> <P>Например, если пользователь класса Date захочет объявить функции today и clearAppointments, ему не следует вручную включать опережающее объявление класса Date, как было показано выше. Вместо этого он должен включить директивой #include соответствующий файл с объявлениями:</P> <source lang="cpp"> #include “datefwd.h” // заголочный файл, в котором объявлен // (но не определен) класс Date Date today(); // как раньше void clearAppointments(Date d); </source> <P>Файл с объявлениями назван «datefwd.h» по аналогии с заголовочным файлом &lt;iosfwd&gt; из стандартной библиотеки C++ ([[Правило 54: Ознакомьтесь со стандартной библиотекой, включая TR1 | см. правило 54]]). &lt;iosfwd&gt; содержит объявления компонентов iostream, определения которых находятся в нескольких разных заголовках, включая &lt;sstream&gt;, &lt;streambuf&gt;, &lt;fstream&gt; и &lt;iostream&gt;.</P> <P>Пример &lt;iosfwd&gt; поучителен еще и по другой причине. Из него следует, что совет этого правила относится в равной мере к шаблонным и обычным классам. Хотя в [[Правило 30: Тщательно обдумывайте использование встроенных функций | правиле 30]] объяснено, что во многих средах разработки программ определения шаблонов обычно находятся в заголовочных файлах, но в некоторых продуктах допускается размещение определений шаблонов и в других местах, поэтому все же имеет смысл предоставить заголовочные файлы, содержащие только объявления, и для шаблонов. &lt;iosfwd&gt; – как раз пример такого файла.</P> <P>В C++ есть также ключевое слово export, позволяющее отделить объявления шаблонов от их определений. К сожалению, поддержка компиляторами этой возможности ограничена, а практический опыт его применения совсем невелик. Сейчас еще слишком рано говорить, какую роль будет играть слово export в эффективном программировании на C++. Классы, подобные Person, в которых используется идиома pimpl, часто называют <EM>классами-дескрипторами</EM> (handle classes). Ответ на вопрос, каким образом работают такие классы, прост: они переадресовывают все вызовы функций соответствующим классам реализаций, которые и выполняют всю реальную работу. Например, вот как могут быть реализованы две функции-члена Person:</P> <source lang="cpp"> #include “Person.h” // поскольку мы реализуем класс Person, // то должны включить его определение #include “PersonImpl.h” // мы должны также включить определение класса // PersonImpl, иначе не сможем вызывать его // функции-члены; отметим, что PersonImpl имеет // в точности те же функции-члены, что и // Person: их интерфейсы идентичны Person::Person(const std::string& name, const Date& birthday, const Address& addr) : pImpl(new Person(name, birthday, addr)) {} std::string Person::name() const { return pImpl->name(); } </source> <P>Обратите внимание на то, как конструктор Person вызывает конструктор Personlmpl (используя new – [[Правило 16: Используйте одинаковые формы new и delete | см. правило 16]]), и как Person::name вызывает PersonImpl::name. Это важный момент. Превращение Person в класс-дескриптор не меняет его поведения – изменяется только место, в котором это поведение реализовано.</P> <P>Альтернативой подходу с использованием класса-дескриптора – сделать Person абстрактным базовым классом специального вида, называемым <EM>интерфейсным классом.</EM> Его назначение – специфицировать интерфейс для производных классов ([[Правило 34: Различайте наследование интерфейса и наследование реализации | см. правило 34]]). В результате он обычно не содержит ни данных-членов, ни конструкторов, но имеет виртуальный деструктор ([[Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе | см. правило 7]]) и набор чисто виртуальных функций, определяющих интерфейс.</P> <P>Интерфейсные классы сродни интерфейсам Java и. NET, но C++ не накладывают на интерфейсные классы тех ограничений, которые присущи этим языкам. Например, ни Java, ни. NET не допускают в интерфейсах наличия членов-данных и реализаций функций-членов. C++ этого не запрещает. Большая гибкость C++ в этом отношении может оказаться кстати. Как объясняется в [[Правило 36: Никогда не переопределяйте наследуемые невиртуальные функции | правиле 36]], реализация невиртуальных функций должна быть одинаковой для всех классов в иерархии, поэтому имеет смысл реализовать такие функции, как часть интерфейсного класса, в котором они объявлены.</P> <P>Интерфейсный класс Person может выглядеть примерно так:</P> <source lang="cpp"> class Person { public: virtual ~Person(); virtual std::string name() const = 0; virtual std::string birthDate() const = 0; virtual std::string address() const = 0; ... }; </source> <P>Пользователи этого класса должны программировать в терминах указателей и ссылок на Person, потому что невозможно создать экземпляр класса, содержащего чисто виртуальные функции (однако можно создавать экземпляры классов, производных от Person – см. далее). Пользователям интерфейсных классов, как и пользователям классов-дескрипторов, нет нужды проводить перекомпиляцию до тех пор, пока не изменяется интерфейс.</P> <P>Конечно, пользователи интерфейсных классов должны иметь способ создавать новые объекты. Обычно они делают это, вызывая функцию, играющую роль конструктора для производных классов, экземпляры которых необходимо создать. Такие функции часто называют функциями-фабриками ([[Правило 13: Используйте объекты для управления ресурсами | см. правило 13]]), или <EM>виртуальными конструкторами.</EM> Они возвращают указатели (и лучше бы интеллектуальные, [[Правило 18: Проектируйте интерфейсы так, что их легко было использовать правильно и трудно – неправильно | см. правило 18]]) на динамически распределенные объекты, которые поддерживают интерфейс интерфейсного класса. Нередко подобные функции объявляют как статические внутри интерфейсного класса:</P> <source lang="cpp"> class Person { public: ... static std::tr1::shared_ptr<Person> // возвращает tr1::shared_ptr create(const std::string& name, // на новый экземпляр Person, const Date& birthday, // инициализированный заданными const Address& addr); // параметрами: см. в правиле 18, ... // почему возвращается }; // tr1::shared_ptr </source> <P>а используют так:</P> <source lang="cpp"> std::string name; Date datefBirth; Address address; ... // создать объект, поддерживающий интерфейс Person std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBrth, address)); ... std::cout << pp->name() // использовать объект через << “ родился ” // интерфейс Person << pp->birthDate() << “ и теперь живет по адресу ” << pp->address(); ... // объект автоматически // удаляется, когда pp выходит // из контекста – см. правило 13 </source> <P>Разумеется, где-то должны быть определены конкретные классы, поддерживающие интерфейс такого интерфейсного класса, и вызваны реальные конструкторы. Все это происходит «за кулисами», внутри файлов, содержащих реализацию виртуальных конструкторов. Например, интерфейсный класс Person может иметь конкретный производный класс RealPerson, предоставляющий реализацию унаследованных виртуальных функций:</P> <source lang="cpp"> class RealPerson public Person { public: RealPerson(const std::string& name, const Date& birthday, const Address& addr) : theName(name), theBirthDate(birthday), theAddress(addr) {} virtual ~RealPerson() {} std::string name() const; // реализация этих функций std::string birthDate() const; // не показана, но ее std::string address() const; // легко представить private: std::string theName; Date theBirthDaye; Address theAddress; }; </source> <P>Имея класс RealPerson, очень легко написать Person::create:</P> <source lang="cpp"> std::tr1::shared_ptr<Person> create( const std::string& name, const Date& birthday, const Address& addr) { return std::tr1::shared_ptr<Person>(new RealPerson(name, birthday, addr)); } </source> <P>Более реалистическая реализация Person::create должна создавать разные типы объектов классов-наследников, в зависимости, например, от дополнительных параметров функции, данных, прочитанных из файла или базы данных, переменных окружения и т. п.</P> <P>RealPerson демонстрирует один из двух наиболее распространенных механизмов реализации интерфейсных классов: он наследует спецификации своего интерфейса от интерфейсного класса Person, а затем реализует функции этого интерфейса. Второй способ реализации интерфейсного класса предполагает использование множественного наследования ([[Правило 40: Продумывайте подход к использованию множественного наследования | см. правило 40]]).</P> <P>Итак, классы-дескрипторы и интерфейсные классы отделяют интерфейс от реализации, уменьшая тем самым зависимости между файлами на этапе компиляции. Теперь, я уверен, вы ждете примечания мелким шрифтом: «Во сколько обойдется этот хитрый фокус?» Цена вполне обычная в мире программирования: некоторое уменьшение скорости выполнения программы плюс дополнительный расход памяти на каждый объект.</P> <P>Применительно к классам-дескрипторам функции-члены должны использовать указатель на реализацию (pImpl), чтобы добраться до данных самого объекта. Для каждого обращения это добавляет один уровень косвенной адресации. Кроме того, к объему памяти, необходимому для хранения каждого объекта, нужно добавить размер указателя. И наконец, указатель на реализацию должен быть инициализирован (в конструкторе класса-дескриптора), чтобы он указывал на динамически распределенный объект реализации; следовательно, вы навлекаете на себя еще и накладные расходы, сопровождающие динамическое выделение памяти и последующее ее освобождение, а также возможность возникновения исключений bad_alloc (из-за недостатка памяти).</P> <P>Для интерфейсных классов каждый вызов функции будет виртуальным, поэтому всякий раз вы платите за косвенный переход ([[Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе | см. правило 7]]). Кроме того, классы, производные от интерфейсного класса, должны содержать указатель на таблицу виртуальных функций (и снова [[Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе | см. правило 7]]). Этот указатель может увеличить объем памяти, необходимый для хранения объекта, в зависимости от того, является ли интерфейсный класс единственным источником виртуальных функций для объекта.</P> <P>И наконец, ни классы-дескрипторы, ни интерфейсные классы не могут извлечь выгоду из использования встроенных функций. В [[Правило 30: Тщательно обдумывайте использование встроенных функций | правиле 30]] объяснено, почему тела потенциально встраиваемых функций должны быть в заголовочных файлах, но классы-дескрипторы и интерфейсные классы специально предназначены для того, чтобы скрыть такие детали реализации, как тело функций.</P> <P>Однако было бы серьезной ошибкой отказываться от классов-дескрипторов и интерфейсных классов только потому, что их использование связано с дополнительными расходами. То же самое можно сказать и о виртуальных функциях, но вы ведь не отказываетесь от их применения. (В противном случае вы читаете не ту книгу.) Рассмотрите возможность использования предлагаемых приемов по мере эволюции ваших программ. Применяйте классы-дескрипторы и интерфейсные классы в процессе разработки, чтобы уменьшить влияние изменений в реализации на пользователей. Если вы можете показать, что различие в скорости и/или размере программы настолько существенно, что во имя повышения эффективности оно оправдывает увеличение зависимости между классами, то на конечной стадии реализации заменяйте их конкретными классами.</P> == Что следует помнить == *Основная идея уменьшения зависимостей на этапе компиляции состоит в том, чтобы заменить зависимость от определения зависимостью от объявления. Эта идея лежит в основе двух подходов: классов-дескрипторов и интерфейсных классов. *Заголовочные файлы библиотек должны существовать в обеих формах: полной и содержащей только объявления. Это справедливо независимо от того, включают они шаблоны или нет. 3ba8ea176026b6964e45833d0c1f31affcd54fd8 Правило 32: Используйте открытое наследование для моделирования отношения «является» 0 32 66 2013-10-03T13:03:37Z Lerom 3360334 Новая страница: «<P>Вильям Демент (William Dement) в своей книге «Кто-то должен бодрствовать, пока остальные спят»…» wikitext text/x-wiki <P>Вильям Демент (William Dement) в своей книге «Кто-то должен бодрствовать, пока остальные спят» (W. H. Freeman and Company, 1974) рассказывает о том, как он пытался донести до студентов наиболее важные идеи своего курса. Утверждается, говорил он своей группе, что средний британский школьник помнит из уроков истории лишь то, что битва при Хастингсе произошла в 1066 году. Даже если ученик почти ничего не запомнил из курса истории, подчеркивает Демент, 1066 год остается в его памяти. Демент пытался внушить слушателям несколько основных идей, в частности ту любопытную истину, что снотворное вызывает бессонницу. Он призывал своих студентов запомнить ряд ключевых фактов, даже если забудется все, что обсуждалось на протяжении курса, и в течение семестра возвращался к нескольким фундаментальным заповедям.</P> <P>Последним на заключительном экзамене был вопрос: «Напишите, какой факт из тех, что обсуждались на лекциях, вы запомните на всю жизнь». Проверяя работы, Демент был ошеломлен. Почти все упомянули 1066 год.</P> <P>Теперь я с трепетом хочу провозгласить, что самое важное правило в объектно-ориентированном программировании на C++ звучит так: открытое наследование означает «является». Твердо запомните это.</P> <P>Если вы пишете класс D (derived – «производный») открыто наследует классу B («base» – «базовый»), то тем самым сообщаете компилятору C++ (а заодно и людям, читающим ваш код), что каждый объект типа D является также объектом типа B, но <EM>не наоборот.</EM> Вы говорите, что B представляет собой более общую концепцию, чем D, а D – более конкретную концепцию, чем B. Вы утверждаете, что везде, где может быть использован объект B, можно использовать также объект D, потому что D является объектом типа B. С другой стороны, если вам нужен объект типа D, то объект B не подойдет, поскольку каждый D «является разновидностью» B, но не наоборот.</P> <P>Такой интерпретации открытого наследования придерживается C++. Рассмотрим следующий пример:</P> <source lang="cpp"> class Person {...}; class Student: public Person {...}; </source> <BR><P>Здравый смысл и опыт подсказывают нам, что каждый студент – человек, но не каждый человек – студент. Именно такую связь подразумевает данная иерархия. Мы ожидаем, что всякое утверждение, справедливое для человека – например, что у него есть дата рождения, – справедливо и для студента, но не все, что верно для студента – например, что он учится в каком-то определенном институте, – верно для человека в общем случае.</P> <P>Применительно к C++ это выглядит следующим образом: любая функция, которая принимает аргумент типа Person (или указатель на Person, или ссылку на Person), примет объект типа Student (или указатель на Student, или ссылку на Student):</P> <source lang="cpp"> void eat(const Person& p); // все люди могут есть void study(const Student& s); // только студент учится Person p; // p – человек Student s; // s – студент eat(p); // правильно, p есть человек eat(s); // правильно, s – это студент, // и студент также является человеком study(s); // правильно study(p); // ошибка! p – не студент </source> <P>Все сказанное верно только для <EM>открытого</EM> наследования. C++ будет вести себя так, как описано выше, только в случае, если Student открыто наследует Person. Закрытое наследование означает нечто совсем иное ([[см. правило 39]]), а смысл защищенного наследования ускользает от меня по сей день.</P> <P>Идея тождества открытого наследования и понятия «является» кажется достаточно очевидной, но иногда интуиция нас подводит. Рассмотрим следующий пример: пингвин – это птица, птицы умеют летать. Если вы по наивности попытаетесь выразить это на C++, то вот что получится:</P> <source lang="cpp"> class Bird { public: virtual void fly(); // птицы умеют летать ... }; class Penguin: public Bird { // пингвины – птицы ... }; </source> <P>Неожиданно мы столкнулись с затруднением. Утверждается, что пингвины могут летать, что, как известно, неверно. В чем тут дело?</P> <P>В данном случае нас подвела неточность разговорного языка. Когда мы говорим, что птицы умеют летать, то не имеем в виду, что <EM>все</EM> птицы летают, а только то, что обычно они обладают такой способностью. Если бы мы выбирали формулировки поточнее, то вспомнили бы, что существует несколько видов нелетающих птиц, и пришли к следующей иерархии, которая значительно лучше моделирует реальность:</P> <source lang="cpp"> class Bird { ... // функция fly не объявлена }; class FlyingBird: public Bird { public: virtual void fly(); ... }; class Penguin: public Bird { ... // функция fly не объявлена }; </source> <P>Данная иерархия гораздо точнее отражает реальность, чем первоначальная.</P> <P>Но и теперь еще не все закончено с «птичьими делами», потому что для некоторых приложений может и не быть необходимости делать различие между летающими и нелетающими птицами. Так, если ваше приложение в основном имеет дело с клювами и крыльями и никак не отражает способность пернатых летать, вполне сойдет и исходная иерархия. Это наблюдение, сообственно, является лишь подтверждением того, что не существует идеального проекта, который подходил бы для всех видов программных систем. Выбор проекта зависит от того, что система должна делать – как сейчас, так и в будущем. Если ваше приложение никак не связано с полетами и не предполагается, что оно будет связано с ними в дальнейшем, то вполне можно не принимать во внимание различий между летающими и нелетающими птицами. На самом деле даже лучше не проводить таких различий, потому что его нет в мире, который вы пытаетесь моделировать. Существует другая школа, иначе относящаяся к рассматриваемой проблеме. Она предлагает переопределить для пингвинов функцию fly() так, чтобы во время исполнения она возвращала ошибку:</P> <source lang="cpp"> void error(const std::string& msg); // определено в другом месте class Penguin: public Bird { public: virtual void fly() {error(“Попытка заставить пингвина летать!”);} ... }; </source> <P>Важно понимать, что это здесь имеется в виду не совсем то, что вам могло показаться. Мы не говорим: «Пингвины не могут летать», а лишь сообщаем: «Пингвины могут летать, но с их стороны было бы ошибкой это делать».</P> <P>В чем разница? Во времени обнаружения ошибки. Утверждение «пингвины не могут летать» может быть поддержано на уровне компилятора, а соответствие утверждения «попытка полета ошибочна для пингвинов» реальному положению дел может быть обнаружено во время выполнения программы.</P> <P>Чтобы обозначить ограничение «пингвины не могут летать – и точка», следует убедиться, что для объектов Penguin функция fly() не определена:</P> <source lang="cpp"> class Bird { ... // функция fly не объявлена }; class Penguin: public Bird { ... // функция fly не объявлена }; </source> <P>Если теперь вы попробуете заставить пингвина взлететь, компилятор сделает вам выговор за нарушение правил:</P> <source lang="cpp"> Penguin p; p.fly(); // ошибка! </source> <P>Это сильно отличается от поведения, которое получается, если применить подход, генерирующий ошибку времени исполнения. Ведь в таком случае компилятор ничего не может сказать о вызове p.fly(). В [[правиле 18]] объясняется, что хороший интерфейс предотвращает компиляцию неверного кода, поэтому лучше выбрать проект, который отвергает попытки пингвинов полетать во время компиляции, а не во время исполнения.</P> <P>Возможно, вы решите, что вам недостает интуиции орнитолога, но вполне можете положиться на свои познания в элементарной геометрии, не так ли? Тогда ответьте на следующий простой вопрос: должен ли класс Square (квадрат) открыто наследовать классу Rectangle (прямоугольник)?</P> [[Файл:Example.jpg]] <P>«Конечно! – скажете вы. – Каждый знает, что квадрат – это прямоугольник, а обратное утверждение в общем случае неверно». Что ж, правильно, по крайней мере, для школы. Но мы ведь решаем задачи посложнее школьных.</P> <source lang="cpp"> class Rectangle { public: virtual void setHeight(int newHeight); virtual void setWidth(int newWidth); virtual int height() const; // возвращают текущие значения virtual int width() const; ... }; void makeBigger(Rectangle& r) // функция увеличивает площадь r { int oldHeight = r.height(); r.setWidth(r.width() + 10); // увеличить ширину r на 10 assert(r.height() == oldHeight); // убедиться, что высота r } // не изменилась </source> <P>Ясно, что утверждение assert никогда не должно нарушаться. Функция make-Bigger изменяет только ширину r. Высота остается постоянной.</P> <P>Теперь рассмотрим код, который посредством открытого наследования позволяет рассматривать квадрат как частный случай прямоугольника:</P> <source lang="cpp"> class Square: public Rectangle {…}; Square s; ... assert(s.width() == s.height()); // должно быть справедливо для // всех квадратов makeBigger(s); // из-за наследования, s является // Rectangle, поэтому мы можем // увеличить его площадь assert(s.width() == s.height()); // По-прежнему должно быть справедливо // для всех квадратов </source> <P>Как и в предыдущем примере, что второе утверждение также никогда не должно быть нарушено. По определению, ширина квадрата равна его высоте.</P> <P>Но теперь перед нами встает проблема. Как примирить следующие утверждения?</P> *Перед вызовом makeBigger высота s равна ширине. *Внутри makeBigger ширина s изменяется, а высота – нет. *После возврата из makeBigger высота s снова равна ширине (отметим, что s передается по ссылке, поэтому makeBigger модифицирует именно s, а не его копию). <P>Так что же?</P> <P>Добро пожаловать в удивительный мир открытого наследования, где интуиция, приобретенная вами в других областях знания, включая математику, иногда оказывается плохим помощником. Основная трудность в данном случае заключается в том, что некоторые утверждения, справедливые для прямоугольника (его ширина может быть изменена независимо от высоты), не выполняются для квадрата (его ширина и высота должны быть одинаковы). Но открытое наследование предполагает, что все, что применимо к объектам базового класса, – <EM>все!</EM> – также применимо и к объектам производных классов. В ситуации с прямоугольниками и квадратами (а также в аналогичных случаях, включая множества и списки из [[правила 38]]), утверждение этого условия не выполняется, поэтому использование открытого наследования для моделирования здесь некорректно. Компилятор, конечно, этого не запрещает, но, как мы только что видели, не существует гарантий, что такой код будет вести себя должным образом. Любому программисту должно быть известно (некоторые знают это лучше других): если код компилируется, то это еще не значит, что он будет работать.</P> <P>Все же не стоит беспокоиться, что приобретенная вами за многие годы разработки программного обеспечения интуиция окажется бесполезной при переходе к объектно-ориентированному программированию. Все ваши знания по-прежнему актуальны, но теперь, когда вы добавили к своему арсеналу наследование, вам придется дополнить свою интуицию новым пониманием, позволяющим создавать приложения с использованием наследования. Со временем идея наследования Penguin от Bird или Square от Rectangle будет казаться вам столь же забавной, как функция объемом в несколько страниц. Такое решение <EM>может</EM> оказаться правильным, но это маловероятно.</P> <P>Отношение «является» – не единственное, возможное между классами. Два других, достаточно распространенных отношения – это «содержит» и «реализован посредством». Они рассматриваются в правилах [[38]] и [[39]]. Очень часто при проектировании на C++ весь проект идет вкривь и вкось из-за того, что эти взаимосвязи моделируются отношением «является». Поэтому вы должны быть уверены, что понимаете различия между этими отношениями и знаете, каким образом их лучше всего моделировать в C++.</P> == Что следует помнить == *Открытое наследование означает «является». Все, что применимо к базовому классу, должно быть применимо также и производным от него, потому что каждый объект производного класса является также объектом базового класса. e0f486eb03b355b7e240b3671a9f9cd5a8e2468f Обсуждение:Правило 4: Прежде чем использовать объекты, убедитесь, что они инициализированы 1 33 67 2013-10-22T04:47:37Z 46.72.71.105 0 Сетка / сетка для заборов - Orelsetka.ru wikitext text/x-wiki Предлагаем купить сетку - решетки сетки из складского наличия. Большой ассортимент сетки со склада г.Орел: производители сетки от лучших производителей на территории России. Продажа сетки Мурманск: сетка металлическая, сетка сварная, сетка тканая, сетка плетеная, сетка рабица, со склада г.Орел. Цены производителя. Продажа сетки оптом и в розницу по низким ценам. Номенклатура поставляемой сетки постоянно расширяется. Широкий ассортимент сетки позволит и Вам отказаться от долгого поиска оптимального поставщика, заказать всё необходимое у нас и при этом сэкономить свои деньги, в том числе на транспортных и других расходах. Специалисты нашей компании всегда готовы предоставить консультацию о любом виде сетки, ответить на все интересующие вопросы, оказать помощь в выборе, оперативно и недорого доставить заказ в Ваш город. Надеемся, что по большинству направлениям нашей деятельности мы сможем установить с Вами прочные и долговременные отношения! Вся продукция сертифицирована и имеет необходимые маркировки. 472fd619bd63329e0778ad03fad22a7715c58e48 68 67 2013-10-26T03:15:07Z 46.72.66.190 0 /* Сетка / сетка для растений - Orelsetka.ru */ новая тема wikitext text/x-wiki Предлагаем купить сетку - решетки сетки из складского наличия. Большой ассортимент сетки со склада г.Орел: производители сетки от лучших производителей на территории России. Продажа сетки Мурманск: сетка металлическая, сетка сварная, сетка тканая, сетка плетеная, сетка рабица, со склада г.Орел. Цены производителя. Продажа сетки оптом и в розницу по низким ценам. Номенклатура поставляемой сетки постоянно расширяется. Широкий ассортимент сетки позволит и Вам отказаться от долгого поиска оптимального поставщика, заказать всё необходимое у нас и при этом сэкономить свои деньги, в том числе на транспортных и других расходах. Специалисты нашей компании всегда готовы предоставить консультацию о любом виде сетки, ответить на все интересующие вопросы, оказать помощь в выборе, оперативно и недорого доставить заказ в Ваш город. Надеемся, что по большинству направлениям нашей деятельности мы сможем установить с Вами прочные и долговременные отношения! Вся продукция сертифицирована и имеет необходимые маркировки. == Сетка / сетка для растений - Orelsetka.ru == Предлагаем купить сетку - сетка штукатурная металлическая из складского наличия. Большой ассортимент сетки со склада г.Орел: сетка для упаковки от лучших производителей на территории России. Продажа сетки Иваново: сетка металлическая, сетка сварная, сетка тканая, сетка плетеная, сетка рабица, со склада г.Орел. Цены производителя. Продажа сетки оптом и в розницу по низким ценам. Номенклатура поставляемой сетки постоянно расширяется. Широкий ассортимент сетки позволит и Вам отказаться от долгого поиска оптимального поставщика, заказать всё необходимое у нас и при этом сэкономить свои деньги, в том числе на транспортных и других расходах. Специалисты нашей компании всегда готовы предоставить консультацию о любом виде сетки, ответить на все интересующие вопросы, оказать помощь в выборе, оперативно и недорого доставить заказ в Ваш город. Надеемся, что по большинству направлениям нашей деятельности мы сможем установить с Вами прочные и долговременные отношения! Вся продукция сертифицирована и имеет необходимые маркировки. 6df7f300d89e1c0bf947dd61e891054e48d84878 69 68 2013-11-11T09:01:56Z 176.194.96.162 0 /* Сетка / сетка рябица - Orelsetka.ru */ новая тема wikitext text/x-wiki Предлагаем купить сетку - решетки сетки из складского наличия. Большой ассортимент сетки со склада г.Орел: производители сетки от лучших производителей на территории России. Продажа сетки Мурманск: сетка металлическая, сетка сварная, сетка тканая, сетка плетеная, сетка рабица, со склада г.Орел. Цены производителя. Продажа сетки оптом и в розницу по низким ценам. Номенклатура поставляемой сетки постоянно расширяется. Широкий ассортимент сетки позволит и Вам отказаться от долгого поиска оптимального поставщика, заказать всё необходимое у нас и при этом сэкономить свои деньги, в том числе на транспортных и других расходах. Специалисты нашей компании всегда готовы предоставить консультацию о любом виде сетки, ответить на все интересующие вопросы, оказать помощь в выборе, оперативно и недорого доставить заказ в Ваш город. Надеемся, что по большинству направлениям нашей деятельности мы сможем установить с Вами прочные и долговременные отношения! Вся продукция сертифицирована и имеет необходимые маркировки. == Сетка / сетка для растений - Orelsetka.ru == Предлагаем купить сетку - сетка штукатурная металлическая из складского наличия. Большой ассортимент сетки со склада г.Орел: сетка для упаковки от лучших производителей на территории России. Продажа сетки Иваново: сетка металлическая, сетка сварная, сетка тканая, сетка плетеная, сетка рабица, со склада г.Орел. Цены производителя. Продажа сетки оптом и в розницу по низким ценам. Номенклатура поставляемой сетки постоянно расширяется. Широкий ассортимент сетки позволит и Вам отказаться от долгого поиска оптимального поставщика, заказать всё необходимое у нас и при этом сэкономить свои деньги, в том числе на транспортных и других расходах. Специалисты нашей компании всегда готовы предоставить консультацию о любом виде сетки, ответить на все интересующие вопросы, оказать помощь в выборе, оперативно и недорого доставить заказ в Ваш город. Надеемся, что по большинству направлениям нашей деятельности мы сможем установить с Вами прочные и долговременные отношения! Вся продукция сертифицирована и имеет необходимые маркировки. == Сетка / сетка рябица - Orelsetka.ru == Предлагаем купить сетку - сетка черная из складского наличия. Большой ассортимент сетки со склада г.Орел: сетка крупная от лучших производителей на территории России. Продажа сетки Республика Ингушетия: сетка металлическая, сетка сварная, сетка тканая, сетка плетеная, сетка рабица, со склада г.Орел. Цены производителя. Продажа сетки оптом и в розницу по низким ценам. Номенклатура поставляемой сетки постоянно расширяется. Широкий ассортимент сетки позволит и Вам отказаться от долгого поиска оптимального поставщика, заказать всё необходимое у нас и при этом сэкономить свои деньги, в том числе на транспортных и других расходах. Специалисты нашей компании всегда готовы предоставить консультацию о любом виде сетки, ответить на все интересующие вопросы, оказать помощь в выборе, оперативно и недорого доставить заказ в Ваш город. Надеемся, что по большинству направлениям нашей деятельности мы сможем установить с Вами прочные и долговременные отношения! Вся продукция сертифицирована и имеет необходимые маркировки. d85de45d87d7dd3886698aaa5451654a00ca50a7 70 69 2014-01-29T16:03:25Z 95.107.55.140 0 /* Канаты стальные по ГОСТ Биробиджан */ новая тема wikitext text/x-wiki Предлагаем купить сетку - решетки сетки из складского наличия. Большой ассортимент сетки со склада г.Орел: производители сетки от лучших производителей на территории России. Продажа сетки Мурманск: сетка металлическая, сетка сварная, сетка тканая, сетка плетеная, сетка рабица, со склада г.Орел. Цены производителя. Продажа сетки оптом и в розницу по низким ценам. Номенклатура поставляемой сетки постоянно расширяется. Широкий ассортимент сетки позволит и Вам отказаться от долгого поиска оптимального поставщика, заказать всё необходимое у нас и при этом сэкономить свои деньги, в том числе на транспортных и других расходах. Специалисты нашей компании всегда готовы предоставить консультацию о любом виде сетки, ответить на все интересующие вопросы, оказать помощь в выборе, оперативно и недорого доставить заказ в Ваш город. Надеемся, что по большинству направлениям нашей деятельности мы сможем установить с Вами прочные и долговременные отношения! Вся продукция сертифицирована и имеет необходимые маркировки. == Сетка / сетка для растений - Orelsetka.ru == Предлагаем купить сетку - сетка штукатурная металлическая из складского наличия. Большой ассортимент сетки со склада г.Орел: сетка для упаковки от лучших производителей на территории России. Продажа сетки Иваново: сетка металлическая, сетка сварная, сетка тканая, сетка плетеная, сетка рабица, со склада г.Орел. Цены производителя. Продажа сетки оптом и в розницу по низким ценам. Номенклатура поставляемой сетки постоянно расширяется. Широкий ассортимент сетки позволит и Вам отказаться от долгого поиска оптимального поставщика, заказать всё необходимое у нас и при этом сэкономить свои деньги, в том числе на транспортных и других расходах. Специалисты нашей компании всегда готовы предоставить консультацию о любом виде сетки, ответить на все интересующие вопросы, оказать помощь в выборе, оперативно и недорого доставить заказ в Ваш город. Надеемся, что по большинству направлениям нашей деятельности мы сможем установить с Вами прочные и долговременные отношения! Вся продукция сертифицирована и имеет необходимые маркировки. == Сетка / сетка рябица - Orelsetka.ru == Предлагаем купить сетку - сетка черная из складского наличия. Большой ассортимент сетки со склада г.Орел: сетка крупная от лучших производителей на территории России. Продажа сетки Республика Ингушетия: сетка металлическая, сетка сварная, сетка тканая, сетка плетеная, сетка рабица, со склада г.Орел. Цены производителя. Продажа сетки оптом и в розницу по низким ценам. Номенклатура поставляемой сетки постоянно расширяется. Широкий ассортимент сетки позволит и Вам отказаться от долгого поиска оптимального поставщика, заказать всё необходимое у нас и при этом сэкономить свои деньги, в том числе на транспортных и других расходах. Специалисты нашей компании всегда готовы предоставить консультацию о любом виде сетки, ответить на все интересующие вопросы, оказать помощь в выборе, оперативно и недорого доставить заказ в Ваш город. Надеемся, что по большинству направлениям нашей деятельности мы сможем установить с Вами прочные и долговременные отношения! Вся продукция сертифицирована и имеет необходимые маркировки. == Канаты стальные по ГОСТ Биробиджан == Канаты стальные(тросы) ГОСТ 2688-80 ф 4,1 - 56,0 мм, ГОСТ 7668, 7669 и др. Применяются, в основном, как грузовые канаты на кранах, талях, лебедках для подъема и транспортировки различных грузов. Черные и оцинкованные по группам "С", "Ж" и "ОЖ". Грузовые и грузолюдские для лифтов, талей, вантовые, грозозащитные, арматурные, спиральные, подьемные. Осуществляем отгрузку грозотросов по России собственным автотранспортом, а также отправляем по железной дороге или через транспортные компании. Резка канатов на куски от 100 п.м. f4f0772362f47c563fe2474c82ffab0cf0c7401d 71 70 2014-01-31T14:54:49Z 95.107.55.140 0 /* Канаты стальные по ГОСТ Республика Хакасия */ новая тема wikitext text/x-wiki Предлагаем купить сетку - решетки сетки из складского наличия. Большой ассортимент сетки со склада г.Орел: производители сетки от лучших производителей на территории России. Продажа сетки Мурманск: сетка металлическая, сетка сварная, сетка тканая, сетка плетеная, сетка рабица, со склада г.Орел. Цены производителя. Продажа сетки оптом и в розницу по низким ценам. Номенклатура поставляемой сетки постоянно расширяется. Широкий ассортимент сетки позволит и Вам отказаться от долгого поиска оптимального поставщика, заказать всё необходимое у нас и при этом сэкономить свои деньги, в том числе на транспортных и других расходах. Специалисты нашей компании всегда готовы предоставить консультацию о любом виде сетки, ответить на все интересующие вопросы, оказать помощь в выборе, оперативно и недорого доставить заказ в Ваш город. Надеемся, что по большинству направлениям нашей деятельности мы сможем установить с Вами прочные и долговременные отношения! Вся продукция сертифицирована и имеет необходимые маркировки. == Сетка / сетка для растений - Orelsetka.ru == Предлагаем купить сетку - сетка штукатурная металлическая из складского наличия. Большой ассортимент сетки со склада г.Орел: сетка для упаковки от лучших производителей на территории России. Продажа сетки Иваново: сетка металлическая, сетка сварная, сетка тканая, сетка плетеная, сетка рабица, со склада г.Орел. Цены производителя. Продажа сетки оптом и в розницу по низким ценам. Номенклатура поставляемой сетки постоянно расширяется. Широкий ассортимент сетки позволит и Вам отказаться от долгого поиска оптимального поставщика, заказать всё необходимое у нас и при этом сэкономить свои деньги, в том числе на транспортных и других расходах. Специалисты нашей компании всегда готовы предоставить консультацию о любом виде сетки, ответить на все интересующие вопросы, оказать помощь в выборе, оперативно и недорого доставить заказ в Ваш город. Надеемся, что по большинству направлениям нашей деятельности мы сможем установить с Вами прочные и долговременные отношения! Вся продукция сертифицирована и имеет необходимые маркировки. == Сетка / сетка рябица - Orelsetka.ru == Предлагаем купить сетку - сетка черная из складского наличия. Большой ассортимент сетки со склада г.Орел: сетка крупная от лучших производителей на территории России. Продажа сетки Республика Ингушетия: сетка металлическая, сетка сварная, сетка тканая, сетка плетеная, сетка рабица, со склада г.Орел. Цены производителя. Продажа сетки оптом и в розницу по низким ценам. Номенклатура поставляемой сетки постоянно расширяется. Широкий ассортимент сетки позволит и Вам отказаться от долгого поиска оптимального поставщика, заказать всё необходимое у нас и при этом сэкономить свои деньги, в том числе на транспортных и других расходах. Специалисты нашей компании всегда готовы предоставить консультацию о любом виде сетки, ответить на все интересующие вопросы, оказать помощь в выборе, оперативно и недорого доставить заказ в Ваш город. Надеемся, что по большинству направлениям нашей деятельности мы сможем установить с Вами прочные и долговременные отношения! Вся продукция сертифицирована и имеет необходимые маркировки. == Канаты стальные по ГОСТ Биробиджан == Канаты стальные(тросы) ГОСТ 2688-80 ф 4,1 - 56,0 мм, ГОСТ 7668, 7669 и др. Применяются, в основном, как грузовые канаты на кранах, талях, лебедках для подъема и транспортировки различных грузов. Черные и оцинкованные по группам "С", "Ж" и "ОЖ". Грузовые и грузолюдские для лифтов, талей, вантовые, грозозащитные, арматурные, спиральные, подьемные. Осуществляем отгрузку грозотросов по России собственным автотранспортом, а также отправляем по железной дороге или через транспортные компании. Резка канатов на куски от 100 п.м. == Канаты стальные по ГОСТ Республика Хакасия == Тросы стальные(тросы) ГОСТ 2688-80 ф 4,1 - 56,0 мм, ГОСТ 7668, 7669 и др. Применяются, в основном, как грузовые канаты на кранах, талях, лебедках для подъема и транспортировки различных грузов. Черные и оцинкованные по группам "С", "Ж" и "ОЖ". Грузовые и грузолюдские для лифтов, талей, вантовые, грозозащитные, арматурные, спиральные, подьемные. Осуществляем доставку грозотросов по России собственным автотранспортом, а также отправляем по железной дороге или через транспортные компании. Резка канатов на куски от 100 п.м. 44789404cd0d1767e5183793e2be828db3a4a4ac 72 71 2014-02-24T20:32:13Z 95.107.55.140 0 /* Канаты стальные по ГОСТ Пензенская область */ новая тема wikitext text/x-wiki Предлагаем купить сетку - решетки сетки из складского наличия. Большой ассортимент сетки со склада г.Орел: производители сетки от лучших производителей на территории России. Продажа сетки Мурманск: сетка металлическая, сетка сварная, сетка тканая, сетка плетеная, сетка рабица, со склада г.Орел. Цены производителя. Продажа сетки оптом и в розницу по низким ценам. Номенклатура поставляемой сетки постоянно расширяется. Широкий ассортимент сетки позволит и Вам отказаться от долгого поиска оптимального поставщика, заказать всё необходимое у нас и при этом сэкономить свои деньги, в том числе на транспортных и других расходах. Специалисты нашей компании всегда готовы предоставить консультацию о любом виде сетки, ответить на все интересующие вопросы, оказать помощь в выборе, оперативно и недорого доставить заказ в Ваш город. Надеемся, что по большинству направлениям нашей деятельности мы сможем установить с Вами прочные и долговременные отношения! Вся продукция сертифицирована и имеет необходимые маркировки. == Сетка / сетка для растений - Orelsetka.ru == Предлагаем купить сетку - сетка штукатурная металлическая из складского наличия. Большой ассортимент сетки со склада г.Орел: сетка для упаковки от лучших производителей на территории России. Продажа сетки Иваново: сетка металлическая, сетка сварная, сетка тканая, сетка плетеная, сетка рабица, со склада г.Орел. Цены производителя. Продажа сетки оптом и в розницу по низким ценам. Номенклатура поставляемой сетки постоянно расширяется. Широкий ассортимент сетки позволит и Вам отказаться от долгого поиска оптимального поставщика, заказать всё необходимое у нас и при этом сэкономить свои деньги, в том числе на транспортных и других расходах. Специалисты нашей компании всегда готовы предоставить консультацию о любом виде сетки, ответить на все интересующие вопросы, оказать помощь в выборе, оперативно и недорого доставить заказ в Ваш город. Надеемся, что по большинству направлениям нашей деятельности мы сможем установить с Вами прочные и долговременные отношения! Вся продукция сертифицирована и имеет необходимые маркировки. == Сетка / сетка рябица - Orelsetka.ru == Предлагаем купить сетку - сетка черная из складского наличия. Большой ассортимент сетки со склада г.Орел: сетка крупная от лучших производителей на территории России. Продажа сетки Республика Ингушетия: сетка металлическая, сетка сварная, сетка тканая, сетка плетеная, сетка рабица, со склада г.Орел. Цены производителя. Продажа сетки оптом и в розницу по низким ценам. Номенклатура поставляемой сетки постоянно расширяется. Широкий ассортимент сетки позволит и Вам отказаться от долгого поиска оптимального поставщика, заказать всё необходимое у нас и при этом сэкономить свои деньги, в том числе на транспортных и других расходах. Специалисты нашей компании всегда готовы предоставить консультацию о любом виде сетки, ответить на все интересующие вопросы, оказать помощь в выборе, оперативно и недорого доставить заказ в Ваш город. Надеемся, что по большинству направлениям нашей деятельности мы сможем установить с Вами прочные и долговременные отношения! Вся продукция сертифицирована и имеет необходимые маркировки. == Канаты стальные по ГОСТ Биробиджан == Канаты стальные(тросы) ГОСТ 2688-80 ф 4,1 - 56,0 мм, ГОСТ 7668, 7669 и др. Применяются, в основном, как грузовые канаты на кранах, талях, лебедках для подъема и транспортировки различных грузов. Черные и оцинкованные по группам "С", "Ж" и "ОЖ". Грузовые и грузолюдские для лифтов, талей, вантовые, грозозащитные, арматурные, спиральные, подьемные. Осуществляем отгрузку грозотросов по России собственным автотранспортом, а также отправляем по железной дороге или через транспортные компании. Резка канатов на куски от 100 п.м. == Канаты стальные по ГОСТ Республика Хакасия == Тросы стальные(тросы) ГОСТ 2688-80 ф 4,1 - 56,0 мм, ГОСТ 7668, 7669 и др. Применяются, в основном, как грузовые канаты на кранах, талях, лебедках для подъема и транспортировки различных грузов. Черные и оцинкованные по группам "С", "Ж" и "ОЖ". Грузовые и грузолюдские для лифтов, талей, вантовые, грозозащитные, арматурные, спиральные, подьемные. Осуществляем доставку грозотросов по России собственным автотранспортом, а также отправляем по железной дороге или через транспортные компании. Резка канатов на куски от 100 п.м. == Канаты стальные по ГОСТ Пензенская область == Тросы стальные(тросы) ГОСТ 2688-80 ф 4,1 - 56,0 мм, ГОСТ 7668, 7669 и др. Применяются, в основном, как грузовые канаты на кранах, талях, лебедках для подъема и транспортировки различных грузов. Черные и оцинкованные по группам "С", "Ж" и "ОЖ". Грузовые и грузолюдские для лифтов, талей, вантовые, грозозащитные, арматурные, спиральные, подьемные. Осуществляем доставку грозотросов по России собственным автотранспортом, а также отправляем по железной дороге или через транспортные компании. Резка канатов на куски от 100 п.м. 45f87b2d2695579cdfe150c2b58b8717c74448b9 Обсуждение:Заглавная страница 1 34 73 2015-09-15T11:17:02Z 94.141.36.130 0 Люблю Россию wikitext text/x-wiki Люблю Россию b4dcad8fab84af385fc2d3561241d179a28bbfc3 Справка:Staff 12 35 74 2017-01-06T11:25:11Z 109.86.72.150 0 Требуются водители для работы в Гет такси wikitext text/x-wiki Ищем водителей для работы по заказам Гет такси (Get taxi) на своем авто или арендованном/раскатном. - Минимальная комиссия 20% (gett берет 17,7% и мы всего 2,3%) - Выплаты каждую неделю.(есть ежедневные выплаты) - Подключение в течении 10-15 минут. - График работы определяете вы сами. -Максимально оперативное решение всех возникших вопросов. - Отсутствие холостого пробега. Огромная клиентская аудитория по всему городу! Внимание - выдаем талоны на бензин Для подключения необходимо: -Документ на машину -Паспорт -Права -Лицензия (если ее нет , поможем получить) При этом не требуется наличие ИП! -Устройство телефон или планшет на базе Android или IOS. Ждем вас в наш дружный коллектив Звоните 8-800-333-04-42 52d1460e4d6fddf7f4e26bda2759ec980c59457d Правило 3: Везде, где только можно используйте const 0 5 75 18 2017-10-23T16:56:47Z Xxcapog 30460010 /* Как избежать дублирования в константных и неконстантных функциях-членах */ wikitext text/x-wiki Замечательное свойство модификатора const состоит в том, что он накладывает определенное семантическое ограничение: данный объект не должен модифицироваться, – и компилятор будет проводить это ограничение в жизнь. const позволяет указать компилятору и программистам, что определенная величина должна оставаться неизменной. Во всех подобных случаях вы должны обозначить это явным образом, призывая себе на помощь компилятор и гарантируя тем самым, что ограничение не будет нарушено.<br> Ключевое слово const удивительно многосторонне. Вне классов вы можете использовать его для определения констант в глобальной области или в пространстве имен ([[см. правило 2]]), а также для статических объектов (внутри файла, функции или блока). Внутри классов допустимо применять его как для статических, так и для нестатических данных-членов. Для указателей можно специфицировать, должен ли быть константным сам указатель, данные, на которые он указывает, либо и то, и другое (или ни то, ни другое): <source lang="cpp"> char greeting[] = “Hello”; char *p = greeting; // неконстантный указатель, // неконстантные данные const char *p = greeting; // неконстантный указатель, // константные данные char * const p = greeting; // константный указатель, // неконстантные данные const char * const p = greeting; // константный указатель, // константные данные </source> Этот синтаксис не так страшен, как может показаться. Если слово const появляется слева от звездочки, константным является то, на что указывает указатель; если справа, то сам указатель является константным. Наконец, если же слово const появляется с обеих сторон, то константно и то, и другое.<br> Когда то, на что указывается, – константа, некоторые программисты ставят const перед идентификатором типа. Другие – после идентификатора типа, но перед звездочкой. Семантической разницы здесь нет, поэтому следующие функции принимают параметр одного и того же типа: <source lang="cpp"> void f1(const Widget *pw); // f1 принимает указатель на // константный объект Widget void f2(Widget const *pw); // то же самое делает f2 </source> Поскольку в реальном коде встречаются обе формы, следует привыкать и к той, и к другой.<br> Итераторы STL смоделированы на основе указателей, поэтому iterator ведет себя почти как указатель T*. Объявление const-итератора подобно объявлению const-указателя (то есть записи T* const): итератор не может начать указывать на что-то другое, но то, на что он указывает, может быть модифицировано. Если вы хотите иметь итератор, который указывал бы на нечто, что запрещено модифицировать (то есть STL-аналог указателя const T*), то вам понадобится константный итератор: <source lang="cpp"> std::vector vec; ... const std::vector::iterator iter = // iter работает как T* const vec.begin(); *iter = 10; // Ok, изменяется то, на что // указывает iter ++iter; // ошибка! iter константный std::vector::const_iterator citer = // citer работает как const T* vec.begin(); *citer = 10; // ошибка! *citer константный ++citer; // нормально, citer изменяется </source> Некоторые из наиболее интересных применений const связаны с объявлениями функций. В этом случае const может относиться к возвращаемому функцией значению, к отдельным параметрам, а для функций-членов – еще и к функции в целом.<br> Если указать в объявлении функции, что она возвращает константное значение, то можно уменьшить количество ошибок в клиентских программах, не снижая уровня безопасности и эффективности. Например, рассмотрим объявление функции operator* для рациональных чисел, введенное в [[правиле 24]]: <source lang="cpp"> class Rational {…} const Rational operator*(const Rational& lhs, const Rational& rhs); </source> Многие программисты удивятся, впервые увидев такое объявление. Почему результат функции operator* должен быть константным объектом? Потому что в противном случае пользователь получил бы возможность делать вещи, которые иначе как надругательством над здравым смыслом не назовешь: <source lang="cpp"> Rational a, b, c; … (a*b)=c; // присваивание произведению a*b! </source> Я не знаю, с какой стати программисту пришло бы в голову присваивать значение произведению двух чисел, но могу точно сказать, что иногда такое может случиться по недосмотру. Достаточно простой опечатки (при условии, что тип может быть преобразован к bool): <source lang="cpp"> if (a*b = c)... // имелось в виду сравнение! </source> Такой код был бы совершенно некорректным, если бы a и b имели встроенный тип. Одним из критериев качества пользовательских типов является совместимость со встроенными (см. также правило 18), а возможность присваивания значения результату произведения двух объектов представляется мне весьма далекой от совместимости. Если же объявить, что operator* возвращает константное значение, то такая ситуация станет невозможной. Вот почему Так Следует Поступать.<br> В отношении аргументов с модификатором const трудно сказать что-то новое; они ведут себя как локальные константные const-объекты. Всюду, где возможно, добавляйте этот модификатор. Если модифицировать аргумент или локальный объект нет необходимости, объявите его как const. Вам всего-то придется набрать шесть символов, зато это предотвратит досадные ошибки типа «хотел напечатать ==, а нечаянно напечатал =» (к чему это приводит, мы только что видели). == Константные функции-члены == Назначение модификатора const в объявлении функций-членов – определить, какие из них можно вызывать для константных объектов. Такие функции-члены важны по двум причинам. Во-первых, они облегчают понимание интерфейса класса, ведь полезно сразу видеть, какие функции могут модифицировать объект, а какие нет. Во-вторых, они обеспечивают возможность работать с константными объектами. Это очень важно для написания эффективного кода, потому что, как объясняется в [[Правило 20: Предпочитайте передачу по ссылке на const передаче по значению | правиле 20]], один из основных способов повысить производительность программ на C++ – передавать объекты по ссылке на константу. Но эта техника будет работать только в случае, когда функции-члены для манипулирования константными объектами объявлены с модификатором const.<br> Многие упускают из виду, что функции, отличающиеся только наличием const в объявлении, могут быть перегружены. Это, однако, важное свойство C++. Рассмотрим класс, представляющий блок текста: <source lang="cpp"> class TextBlock { public: ... const char& operator[](std::size_t position) const // operator[] для {return text[position];} // константных объектов char& operator[](std::size_t position) // operator[] для {return text[position];} // неконстантных объектов private: std::string text; }; </source> Функцию operator[] в классе TextBlock можно использовать следующим образом: <source lang="cpp"> TextBlock tb(“Hello”); Std::cout << tb[0]; // вызов неконстантного // оператора TextBlock::operator[] const TextBlock ctb(“World”); Std::cout << ctb[0]; // вызов константного // оператора TextBlock::operator[] </source> Кстати, константные объекты чаще всего встречаются в реальных программах в результате передачи по указателю или ссылке на константу. Приведенный выше пример ctb является довольно искусственным. Но вот вам более реалистичный: <source lang="cpp"> void print(const TextBlock& ctb) // в этой функции ctb – ссылка // на константный объект { std::cout << ctb[0]; // вызов const TextBlock::operator[] ... } </source> Перегружая operator[] и создавая различные версии с разными возвращаемыми типами, вы можете по-разному обрабатывать константные и неконстантные объекты TextBlock: <source lang="cpp"> std::cout << tb[0]; // нормально – читается // неконстантный TextBlock tb[0] = ‘x’; // нормально – пишется // неконстантный TextBlock std::cout << ctb[0]; // нормально – читается // константный TextBlock ctb[0] = ‘x’; // ошибка! – запись // константного TextBlock </source> Отметим, что ошибка здесь связана только с типом значения, возвращаемого operator[]; сам вызов operator[] проходит нормально. Причина ошибки – в попытке присвоить значение объекту типа const char&, потому что это именно такой тип возвращается константной версией operator[]. Отметим также, что тип, возвращаемый неконстантной версией operator[], – это ссылка на char, а не сам char. Если бы operator[] возвращал просто char, то следующее предложение не скомпилировалось бы: <source lang="cpp"> tb[0] = ‘x’; </source> Это объясняется тем, что возвращаемое функцией значение встроенного типа модифицировать некорректно. Даже если бы это было допустимо, тот факт, что C++ возвращает объекты по значению ([[Правило 20: Предпочитайте передачу по ссылке на const передаче по значению | см. правило 20]]), означал бы следующее: модифицировалась копия tb.text[0], а не само значение tb.text[0]. Вряд ли это то, чего вы ожидаете.<br> Давайте немного передохнем и пофилософствуем. Что означает для функции-члена быть константной? Существует два широко распространенных понятия: побитовая константность (также известная как физическая константность) и логическая константность.<br> Сторонники побитовой константности полагают, что функция-член константна тогда и только тогда, когда она не модифицирует никакие данные-члены объекта (за исключением статических), то есть не модифицирует ни одного бита внутри объекта. Определение побитовой константности хорошо тем, что ее нарушение легко обнаружить: компилятор просто ищет присваивания членам класса. Фактически, побитовая константность – это константность, определенная в C++: функция-член с модификатором const не может модифицировать нестатические данные-члены объекта, для которого она вызвана. К сожалению, многие функции-члены, которые ведут себя далеко не константно, проходят побитовый тест. В частности, функция-член, которая модифицирует то, на что указывает указатель, часто не ведет себя как константная. Но если объекту принадлежит только указатель, то функция формально является побитово константной, и компилятор не станет возражать. Это может привести к неожиданному поведению. Например, предположим, что есть класс подобный Text-Block, где данные хранятся в строках типа char * вместо string, поскольку это необходимо для передачи в функции, написанные на языке C, который не понимает, что такое объекты типа string. <source lang="cpp"> class CtextBlock { public: ... char& operator[](std::size_t position) const // неудачное (но побитово { return pText[position]} // константное) // объявление operator[] private: char *pText; }; </source> В этом классе функция operator[] (неправильно!) объявлена как константная функция-член, хотя она возвращает ссылку на внутренние данные объекта (эта тема обсуждается [[Правило 28: Избегайте возвращения «дескрипторов» внутренних данных | в правиле 28]]). Оставим это пока в стороне и отметим, что реализация operator[] никак не модифицирует pText. В результате компилятор спокойно сгенерирует код для функции operator[]. Ведь она действительно является побитово константной, а это все, что компилятор может проверить. Но посмотрите, что происходит: <source lang="cpp"> const CtextBlock cctb(“Hello”); // объявление константного объекта char &pc = &cctb[0]; // вызов const operator[] для получения // указателя на данные cctb *pc = ‘j’; // cctb теперь имеет значение “Jello” </source> Несомненно, есть что-то некорректное в том, что вы создаете константный объект с определенным значением, вызываете для него только константную функцию-член и тем не менее изменяете его значение!<br> Это приводит нас к понятию логической константности. Сторонники этой философии утверждают, что функции-члены с const могут модифицировать некоторые биты вызвавшего их объекта, но только так, чтобы пользователь не мог этого обнаружить. Например, ваш класс CTextBlock мог бы кэшировать длину текстового блока при каждом запросе: <source lang="cpp"> Class CtextBlock { public: ... std::size_t length() const; private: char *pText; std::size_t textLength; // последнее вычисленное значение длины // текстового блока bool lengthIsValid; // корректна ли длина в данный момент }; std::size_t CtextBlock::length() const { if(!lengthIsValid) { textLength = std::strlen(pText); // ошибка! Нельзя присваивать lengthIsValid = true; // значение textLength и } // lengthIsValid в константной // функции-члене return textLength; } </source> Эта реализация length(), конечно же, не является побитово константной, поскольку может модифицировать значения членов textLength и lengthlsValid. Но в то же время со стороны кажется, что константности объектов CTextBlock это не угрожает. Однако компилятор не согласен. Он настаивает на побитовой константности. Что делать? Решение простое: используйте модификатор mutable. Он освобождает нестатические данные-члены от ограничений побитовой константности: <source lang="cpp"> Class CtextBlock { public: ... std::size_t length() const; private: char *pText; mutable std::size_t textLength; // Эти данные-члены всегда могут быть mutable bool lengthIsValid; // модифицированы, даже в константных }; // функциях-членах std::size_t CtextBlock::length() const { if(!lengthIsValid) { textLength = std::strlen(pText); // теперь порядок lengthIsValid = true; // здесь то же } return textLength; } </source> == Как избежать дублирования в константных и неконстантных функциях-членах == Использование mutable – замечательное решение проблемы, когда побитовая константность вас не вполне устраивает, но оно не устраняет всех трудностей, связанных с const. Например, представьте, что operator[] в классе TextBlock (и CTextBlock) не только возвращает ссылку на соответствующий символ, но также проверяет выход за пределы массива, протоколирует информацию о доступе и, возможно, даже проверяет целостность данных. Помещение всей этой логики в обе версии функции operator[] – константную и неконстантную (даже если забыть, что теперь мы имеем необычно длинные встроенные функции – [[Правило 30: Тщательно обдумывайте использование встроенных функций | см. правило 30]]) – приводит к такому вот неуклюжему коду: <source lang="cpp"> class TextBlock { public: ... const char& operator[](std::size_t position) const { ... // выполнить проверку границ массива ... // протоколировать доступ к данным ... // проверить целостность данных return text[position]; } char& operator[](std::size_t position) const { ... // выполнить проверку границ массива ... // протоколировать доступ к данным ... // проверить целостность данных return text[position]; } private: std::string text; }; </source> Ох! Налицо все неприятности, связанные с дублированием кода: увеличение времени компиляции, размера программы и неудобство сопровождения. Конечно, можно переместить весь код для проверки выхода за границы массива и прочего в отдельную функцию-член (естественно, закрытую), которую будут вызывать обе версии operator[], но обращения к этой функции все же будут дублироваться. В действительности было бы желательно реализовать функциональность operator[] один раз, а использовать в двух местах. То есть одна версия operator[] должна вызывать другую. И это подводит нас к вопросу об отбрасывании константности.<br> С самого начала отметим, отбрасывать константность нехорошо. Я посвятил целое [[Правило 27: Не злоупотребляйте приведением типов | правило 27]] тому, чтобы убедить вас не делать этого, но дублирование кода – тоже не сахар. В данном случае константная версия operator[] делает в точности то же самое, что неконстантная, и отличие между ними – лишь в присутствии модификатора const. В этой ситуации отбрасывать const безопасно, поскольку пользователь, вызывающий неконстантный operator[], так или иначе должен получить неконстантный объект. Ведь в противном случае он не стал бы вызывать неконстантную функцию. Поэтому реализация неконстантного operator[] путем вызова константной версии – это безопасный способ избежать дублирования кода, даже пусть даже для этого требуется воспользоваться оператором const_cast. Ниже приведен получающийся в результате код, но он станет яснее после того, как вы прочитаете следующие далее объяснения: <source lang="cpp"> class TextBlock { public: ... const char& operator[](std::size_t position) const // то же, что и раньше { ... ... ... return text[position]; } char& operator[](std::size_t position) const // теперь просто // вызываем const op[] { return const_cast( // из возвращаемого типа // op[] исключить const static_cast(*this) // добавить const типу // *this [position] // вызвать константную ); // версию op[] } ... }; </source> Как видите, код включает два приведения, а не одно. Мы хотим, чтобы неконстантный operator[] вызывал константный, но если внутри неконстантного оператора [] просто вызовем operator[], то получится рекурсивный вызов. Во избежание бесконечной рекурсии нужно указать, что мы хотим вызвать const operator[], но прямого способа сделать это не существует. Поэтому мы приводим *this от типа TextBlock& к const TextBlock&. Да, мы выполняем приведение, чтобы добавить константность! Таким образом, мы имеем два приведения: одно добавляет константность *this (чтобы был вызван const operator[]), а второе – исключает const из типа возвращаемого значения.<br> Приведение, которое добавляет const, выполняет безопасное преобразование (от неконстантного объекта к константному), поэтому мы используем для этой цели static_cast. Приведение же, которое отбрасывает const, может быть выполнено только с помощью const_cast, поэтому у нас здесь нет выбора. (Строго говоря, выбор есть. Приведение в стиле C также работает, но, как я объясняю в [[Правило 27: Не злоупотребляйте приведением типов | правиле 27]], такие приведения редко являются правильным рещением. Если вы не знакомы с операторами static_cast или const_cast, прочитайте о них в [[Правило 27: Не злоупотребляйте приведением типов | правиле 27]].)<br> Помимо всего прочего, в этом примере мы вызываем оператор, поэтому синтаксис выглядит немного странно. Возможно, этот код не займет приз на конкурсе красоты, зато позволяет достичь нужного эффекта – избежать дублирования посредством реализации неконстантной версии operator[] в терминах константной. И хотя для достижения цели пришлось воспользоваться неуклюжим синтаксисом, который сможете понять только вы сами, однако техника реализации неконстантных функций-членов через неконстантные определенно заслуживает того, чтобы ее знать.<br> А еще нужно иметь в виду, что решать эту задачу наоборот – путем вызова неконстантной версии из константной – неправильно. Помните, что константная функция-член обещает никогда не изменять логическое состояние объекта, а неконстантная не дает таких гарантий. Если вы вызовете неконстантную функцию из константной, то рискуете получить ситуацию, когда объект, который не должен модифицироваться, будет изменен. Вот почему этого не следует делать: чтобы объект не изменился. Фактически, чтобы получить компилируемый код, вам пришлось бы использовать const_cast для отбрасывания константности *this, а это явный признак неудачного решения. Обратная последовательность вызовов – такая, как описана выше, – безопасна. Неконстантная функция-член может делать все, что захочет с объектом, поэтому вызов из нее константной функции-члена ничем не грозит. Потому-то мы и применяем к *this оператор static_cast, отбрасывания константности при этом не происходит.<br> Как я уже упоминал в начале этого правила, модификатор const – чудесная вещь. Для указателей и итераторов; для объектов, на которые ссылаются указатели, итераторы и ссылки; для параметров функций и возвращаемых ими значений; для локальных переменных, для функций-членов – всюду const ваш мощный союзник. Используйте его, где только возможно. Вам понравится! == Что следует помнить == *Объявление чего-либо с модификатором const помогает компиляторам обнаруживать ошибки. const можно использовать с объектами в любой области действия, с параметрами функций и возвращаемых значений, а также с функциями-членами в целом. *Компиляторы проверяют побитовую константность, но вы должны программировать, применяя логическую константность. *Когда константные и неконстантные функции-члены имеют, по сути, одинаковую реализацию, то дублирования кода можно избежать, заставив неконстантную версию вызывать константную. bad9a8917dbfe9a9add2e2a8f8223cc5ff1359aa Раздумываете где сейчас можно будет купить трастовые ссылки? 0 36 76 2022-11-06T19:40:49Z Sonnick84 30703286 Новая страница: «Сделать сайт в наше время возможно будет самыми разными вариантами. Имеются готовые уже...» wikitext text/x-wiki Сделать сайт в наше время возможно будет самыми разными вариантами. Имеются готовые уже конструкторы, там попросту добавляешь необходимые самому себе блоки. Такой способ прекрасно сможет подойти для сайт визитки. В случае если что-то сложнее потребуется, как например многостраничный сайт, то следует просто выбрать хостера и использовать любую CMS. Многие поступают проще еще, приобретая услуги у фирмы в случае если деньги есть или у фрилансера, что выйдет дешевле. Но это только лишь веб сайт, трафик - вот что намного ценнее. В том случае, если проанализируете различные веб-сайты в поисковике, заметите, что часто в ТОП10 попадают по сути одностраничники, на которых не считая рекламы нет ничего. Как такое осуществляется? Вот здесь уже нужно рассказать вам насчет раскрутки сайта, поскольку эта тема весьма рискованная, в том случае, если довериться не мастерам. Невзирая на совершенствование алгоритмов поисковиков, на сегодняшний момент именно статейные прогоны предоставляют быстрый и качественный результат, помогая ресурсу покупателя подняться в ТОПе и конечно получить пользователей. При этом если выбрать хорошего исполнителя, удастся вам немало сэкономить. Наш сервис сможете тут, на котором возможно [https://getmanylinks.ru/ трастовые ссылки купить] и сразу взглянуть на текущие расценки, а кроме того что получите вы. Используем мы специальный софт, он написан был специально под задачи прогона веб сайта. Грамотные спецы выполняют разработку статей, что будут после размещены на трастовых интернет сайтах. Разумеется ждать быстрого эффекта не стоит, однако поверьте нам, он будет! Уже больше семи лет мы работаем в данной сфере, где каждый мастер отвечает лишь за свою задачу, а база постоянно обновляется. Это все вместе позволяет выполнить на самом деле результативный прогон, который поднимет веб-сайт в ТОП и естественно появятся заказчики. Ну а как покупателей уже монетизировать, ваша задача. Однако дать парочку полезных и ценных рекомендаций мы естественно можем, поскольку прогнали успешно огромное количество самых разных веб сайтов и сумеем сразу же увидеть оптимальный вариант для монетизации. Заметим, что цена услуг наших достаточно выгодная, причем качество в итоге отличное. Каким образом этого добиться смогли? За счет отлаженности бизнеса, опыта сотрудников и спец софта, что легко "пробивает" практически любые веб-сайты, давая возможность размещать статьи с ссылками. Проще всего приобрести сразу комплексный прогон, но в том случае, если захотите сначала убедиться в опыте нашем, выберите что-либо одно. На интернет сайте есть подробное сравнение разных тарифных пакетов, непременно почитайте или же отпишитесь по размещенным на сервисе контактам для профессиональной консультации. 2c3051ea28923ed1b3f6f93d405da87318d190f9 Профессиональная помощь и консультации от известных адвокатов 0 37 77 2023-01-12T16:08:31Z Sonnick84 30703286 Профессиональная помощь и консультации от известных адвокатов wikitext text/x-wiki Обратиться можно сейчас к "универсальным" специалистам, которые помочь смогут в любом вопросе. Но в случае если вопрос сложный, естественно лучше обращаться к специализированному специалисту, что имеет многолетний опыт как раз в таком направлении. Тема правоведения обширна и хорошо разбираться везде невозможно. Так что предоставляем разных адвокатов, которые знают собственную область идеально. Чаще всего к нам приходят по делам: аутсорсинга, жилищных споров, конфликтов на самой разной почве, регистрации компании, ДТП. Мы назначаем опытного адвоката, который проконсультирует и конечно поможет победить в суде. Среди основных наших преимуществ, заказчики отмечают: - Сумеем помощь оказать даже в очень сложном деле; - Хорошая репутация; - Стоимость устанавливается индивидуально; - Широкий ассортимент услуг; - Серьезный опыт в судебных процессах. Приблизительные расценки выложены на сайте, выяснить конкретную сумму возможно будет лишь написав менеджеру (онлайн-чат доступен на веб сайте) или лично приехав, это будет куда проще. На интернет сайте компании стали вести рубрику - Блог, где найдете вы подробные статьи на разные темы. К примеру вы узнаете как правильно взыскивать алименты, какие имеются нюансы в данном деле. Как можно будет грамотно и быстро разрешить спор со страховщиком. Всегда стараемся размещать рекомендации по востребованным у клиентов сферам. К примеру материал, который мы сравнительно недавно выложили - [https://pravoedeloperm.ru/trudovoe-pravo/ нормы трудового права], приобрел достаточно серьезную востребованность и популярность и на текущий день его возможно будет встретить перепечатанным на десятках сайтах. В случае если желаете сэкономить или же уверены, что сумеете самолично себя защитить в суде, готовы предложить услуги консультации. Понадобится обо всем сказать честно, после этого юрист подробно расскажет, что именно следует сделать. Но все-таки ключевой нашей деятельностью на сегодняшний момент является - защита в судебных заседаниях. Почитайте в интернете отзывы про нас, либо на тематических форумах. Насчет стоимости волноваться не придется, так как все прописывается в официальном договоре, которому следуем скрупулезно. Причем специалист компании предварительно опишет все, что понадобится, именно поэтому не следует волноваться из-за лишних затрат. 3bcab6c5894f9141ae4a39b9f5f59c105295fc61 Думаете где можно купить маникюрную мебель? 0 38 78 2023-01-13T08:34:01Z Sonnick84 30703286 Думаете где можно купить маникюрную мебель? wikitext text/x-wiki Хорошее оборудование для парикмахерских или же массажных салонов найти несложно. Большой ассортимент предоставляют бесчисленные интернет магазины, а так же можно будет приобрести в зарубежных онлайн магазинах. Тем не менее существуют собственные нюансы при таком выборе, о них коротко расскажем вам. Начнем, итак: 1. Решив выбрать европейские интернет-магазины для приобретения мебели к примеру для парикмахерских, придется отдать довольно таки серьезную сумму. Кроме этого нужно не забывать насчет доставки. Если воспользоваться дешевыми способами, то практически гарантирована поломка оборудования. 2. В случае если оценить отечественные знаменитые онлайн магазины, предоставляющих огромный ассортимент самых разных изделий, в том числе оборудование, то найдете только популярные варианты моделей, а так же бюджетные. Итак, что делать, в случае если надо обставить свою парикмахерскую или массажный салон? Надо найти магазины, что специализируются именно на таких изделиях. Возможно привести как пример довольно знаменитый магазин - оборудование [https://www.imin.ru/equipment/parikmaherskoe-oborudovanie/ в каталоге имин], тут обширный каталог различной мебели. Однако пожалуй стоит кратко обо всем написать. В общем, этот онлайн-магазин своим собственным заказчикам предлагает: • Стоимость в большинстве случаев от изготовителя; • Быструю сборку мебели и оборудования; • Расширенные консультации и помощь при выборе от действительно опытных профессионалов; • Высококачественное косметологическое, маникюрное, массажное, парикмахерское и педикюрное оборудование; • Создание индивидуальной заявки; • Удобные условия доставки. Некоторые покупатели пишут, что в большинстве случаев оборудование стоит гораздо дешевле, нежели чем в иных онлайн магазинах и тут в общем-то нет тайны. Компания собственноручно изготавливает оборудование, а кроме того реализует все используя собственный интернет-магазин. Разумеется цена оказывается в итоге намного дешевле. Здесь можно найти несколько разных вариантов доставки. Однако в принципе все как обычно: самовывоз, а кроме того доставка интернет магазином. Расширенные условия обнаружите по веб ссылке (выше выложили). Наверное стоит подробнее рассказать о специалистах интернет магазина. Здесь продается специфический товар, именно поэтому работники хорошо знают собственный ассортимент, достоинства оборудования и могут посоветовать оптимальный вариант для заказчика. Для этого оставьте заказ, либо позвоните, организуют бесплатную консультацию, а в том случае, если надо будет - сформируют индивидуальную заявку. e79205caab6bc2db459de3591da41c0e868ccd2e Где в наше время выгодно возможно приобрести полис ОСАГО для авто? 0 39 79 2023-01-14T15:08:44Z Sonnick84 30703286 Где в наше время выгодно возможно приобрести полис ОСАГО для авто? wikitext text/x-wiki Сегодня найти возможно будет надежного страховщика, предоставляющего в действительности комфортные условия, а так же выгодные цены. Тем не менее компаний, которые предлагают ОСАГО и КАСКО немало, и иногда выбрать становится сложно. Здесь помочь сможет известный агрегатор, что собственноручно изучит предложения и подберет идеальный вариант по цене. В случае если зайдете на наш проект, сможете сэкономить свое собственное время, ведь мы отлично разбираемся в подобной сфере и понимаем различные нюансы и мелочи в вопросе поиска страховщиков. Подчеркнем, что тщательно выслушиваем ключевые пожелания клиента, подбирая подходящий вариант. Не считая всего этого на текущий момент не предлагаем сопутствующих услуг, а так же можете сразу забыть о комиссии, потому что работаем мы напрямую со всеми страховщиками. Клиенты, обратившись в нашу фирму, через время вновь заявку размещают, успев оценить удобство, низкую цену, оперативность и качество. Кроме этого ввели рубрику, где выкладываем полезные статьи. Например достаточно серьезную известность приобрел материал - [https://xn--80aeeytd1a.xn--p1ai/dogovor-kupli-prodazh-avtomobilya/ акт приема - передачи транспортного средства], где подробным образом про все говорится, а кроме этого размещены советы экспертов. Тем не менее в том случае, если планируете сэкономить себе время, попросту опубликуйте заказ на веб-сайте и консультант перезвонит вскоре. Почему невзирая на возможность обратиться к страховщикам, многие клиенты заказывают у нас в компании услуги? Тут следует рассказать о преимуществах, которые замечают клиенты в собственных отзывах. Подробности смотрите на веб сайте, сейчас расскажем только лишь вкратце. Итак: - Грамотные сотрудники и быстрое оформление документа; - Имеется обширный каталог честных и надежных страховщиков; - Возможно будет узнать конкретную стоимость на интернет сайте, не понадобится звонить или писать в чат менеджеру. Вместе с этим необходимо написать об иных услугах, что можем предоставить собственному клиенту. Получение диагностической карты, а так же техосмотра. Выполняем проверку машины качественно и быстро, не потребуется ожидать в очередях. Имеются станции по СПб, благодаря чему получите возможность на сайте выбрать ближайшую к вам. В том случае, если нужно купить или же продать авто, мы тоже сможем помочь в этом деле. Бесплатно проверяем авто по разным базам, предлагаем ОСАГО, используется качественная защита документа через QR-CODE. Итак, в том случае, если необходима помощь спецов, вы уже знаете куда следует обратиться! 3c4fd455e8ea0eb174f0b3856656150023d66d2e Безопасная покупка любой крипты. Лучший обменник! 0 40 80 2023-01-16T09:06:45Z Sonnick84 30703286 Безопасная покупка любой крипты. Лучший обменник! wikitext text/x-wiki Сервисов для обмена, которые позволяют продать, либо приобрести самые разные варианты криптовалюты, очень много сейчас. Вот только как подобрать действительно лучший? Для того, чтобы проще было разобраться в этой теме, приняли решение описать основные преимущества надежного и проверенного проекта для обмена криптовалюты на примере своего. Большие сделки проводятся в большинстве своем оффлайн. Так например на сегодняшний момент у нас больше 22 офисов, туда можно будет подъехать и при специалисте фирмы произвести продажу или же обмен крипты. Можно разумеется сделать все онлайн, но если обменник в действительности планирует предоставлять комфортные условия собственным клиентам, обязательно наличие офисов. В том случае, если так к примеру сервис [https://vipbtc.org/ https://vipbtc.org/] предлагает подобную услугу, то тогда возможно будет доверять. Хотя посоветуем также обратить особое внимание на инкассацию и охрану, достаточно полезные вещи. Профессиональный сервис обмена всегда предлагает своему собственному участнику разнообразные инструменты, например как графики изменений курса. Благодаря этому можно будет выбрать лучший момент для того, чтобы осуществить покупку или же обмен крипты. Довольно полезная и важная функция, она позволяет избежать применения дополнительных сторонних сервисов, а соответственно снизить риск обмана. На нашем веб-сайте естественно существуют современные инструменты для отслеживания изменений курса. Большинство специалистов заявляют, что наличие грамотной поддержки обязательно на сегодняшний момент для большой компании, которая желает сформировать хорошую репутацию. Сотрудники нашего сервиса имеют не считая огромного опыта, все необходимые инструменты, что в действительности помочь смогут, приехавшему клиенту, рассчитывать на выгодную сделку. Существует немало обменников, использующих активы других фирм. Это действительно намного будет проще, нежели чем использовать собственные деньги. Но в этом случае в случае если клиент рассчитывает осуществить [https://vipbtc.org/obmen/bitcoin-sberbank/ вывод биткоинов на карту сбербанка], будут дополнительные траты, а так же существенная потеря личного времени. При запуске своего сервиса приняли решение применить только лишь свои собственные активы. Благодаря чему стоимость обмена действительно выгодная, ну а время проведения сделки на сегодняшний день составит меньше 15 минут. Сейчас предоставили только лишь короткие советы по выбору надежного обменника. Но скорее всего понимаете куда обращаться, в случае если надо купить или обменять крипту. 1bfd3d8f4d148d12345568c18531fcaa9810a15f Где возможно заказать хороший камин с монтажом? 0 41 81 2023-01-19T12:39:05Z Sonnick84 30703286 Где возможно заказать хороший камин с монтажом? wikitext text/x-wiki Большинство людей, в том случае, если понадобилось им заказать печь, либо камин, почему-то полагают, что возможно будет просто перейти в любой интернет магазин, где реализуется подобная продукция и подобрать идеально подходящую модель по цене и конечно дизайну. Но мы прекрасно знаем то, что в вопросах приобретения камина или же печи есть десятки различных мелочей и нюансов. Вот про них решили в сегодняшнем спец обзоре и рассказать. Если думаете приобрести обычный камин для декоратива, то разумеется это легко: переходите в онлайн-магазин, оставляете заказ, ожидаете доставку, выполняете установку и наслаждаетесь. Если нужен именно полноценный камин или печь, потребуется перед постройкой частного дома в план внести. Можно будет разумеется осуществить монтаж потом, но появятся ограничения. Важно подыскать на самом деле проверенный и надежный интернет-магазин, в котором заказать можно будет высококачественные печи и камины. Вместе с этим отметим, делать выбор на основе каталога не следует. Сейчас легко можно оформить официальные договора с различными производителями и отладить работу магазина. Куда труднее найти действительно надежных и проверенных производителей, организовать группу специалистов, что сумеют помочь заказчику качественно и быстро осуществить установку любого камина. Так например человек, ищущий где [https://kaminline.ru/biokamini/ купить биокамины], должен обратить внимание на непосредственно сам магазин, их сотрудников, а кроме этого опыт и отзывы. Разумеется выбор тоже достаточно важен, тем не менее очень много на сегодняшний день производителей, предоставляющих по небольшой цене красивые модели каминов, но низкого качества. Так что в вопросе поиска онлайн-магазина, не торопитесь. Можно будет дать рекомендацию по выбору интернет-магазина - узнайте, предлагают ли камин под ключ. Естественно это чуть дороже будет, но подобная возможность демонстрирует квалификацию работников онлайн магазина. Нужно помнить всегда, на текущий день для монтажа печи или камина, понадобится подготовительная работа. Поэтому если предложили установку за сутки, порекомендуем отказаться сразу от подобного сотрудничества. Хорошая фирма, несмотря на профессионалов и наличие необходимого инструмента, выполняет работу ориентировочно до 5 дней. Многие почему-то думают, что смогут сами выполнить монтаж камина, либо печи. В том случае, если имеется большой опыт в подобной области, в общем-то подобное возможно вполне. В том случае, если нет опыта, просто потеряете впустую время и деньги. 12878d39597759f5ec4fa877ee6b5dd9e4dd57d8 Куда же позвонить если нужна аренда контейнера? 0 42 82 2023-01-20T11:20:01Z Sonnick84 30703286 Куда же позвонить если нужна аренда контейнера? wikitext text/x-wiki При разработке собственного дела, надо учесть множество различных мелочей и деталей. В случае если их игнорировать, то тогда бизнес обанкротится, и это на сегодняшний день легко подтвердит специалист. При этом в случае если раньше было довольно таки сложно, потому как нужно было внимание уделить изготовлению, покупки, реализации или перевозкам, то сегодня куда проще, есть огромное количество различных фирм, готовых оказать помощь в любом деле. Так например компания наша нашла для себя довольно узкую область, однако весьма востребованную и популярную, особенно для крупных магазинов и компаний, которым требуются в аренду контейнеры. Что же на текущий день в большинстве своем хранят в арендованном контейнере? Зачастую шины, инструмент, автомобили, мебель и оборудование. Возможно разумеется хранить все так например в фуре, однако если оборудование стоит миллионы долларов, нужна получше защита. В общем-то именно поэтому в нашу фирму обращаются клиенты, поскольку предоставляем: • Надежную охрану, а так же камеры наблюдения; • Разные погрузчики; • Освещение; • Огромный каталог разных контейнеров; • Выгодные цены; • Современную антивандальную систему. В том случае, если остались какие-либо сомнения, то надо добавить, на сегодняшний день имеем более двух тысяч постоянных клиентов, некоторые подписали договора на хранение их товара. Итак, каковы главные преимущества для заказчика, что почитав сперва в сети - [https://sklad-24.ru аренда складских помещений], выберет именно наши услуги? По сути ключевые преимущества мы рассказали уже немного выше, тем не менее стоит лишь дополнительно сказать про расценки. В этой сфере имеем уже значительный опыт, свыше двух тысяч разных контейнеров и крупный штат опытных работников. Благодаря этому расценки на услуги довольно низкие, убедиться несложно, оставьте заказ на веб-сайте, консультант вам расскажет про все. На интернет-сайте в подробностях описали варианты оплаты, где конкретно находятся площадки, действующие контакты и цены. Отдельно разработали веб страницу с популярными вопросами, посоветуем почитать. Надо заметить еще один момент, можем осуществить транспортировку любых товаров и их дальнейшую загрузку в контейнеры. Это естественно обойдется вам гораздо дешевле, чем собственноручно выполнить транспортировку. cc3e443c6c823688f6dafe464c93379b548aa210 Где можно в наше время заказать грузовые перевозки по хорошей цене? 0 43 83 2023-01-20T17:05:39Z Sonnick84 30703286 Где можно в наше время заказать грузовые перевозки по хорошей цене? wikitext text/x-wiki В случае если нужны какие-либо товары или же услуги, то как быстро на сегодняшний момент найти возможно лучшую компанию? Можно будет смотреть комментарии и отзывы в сети интернет, сравнивать цены, тщательно оценить материалы, опубликованные на интернет сайте, однако гораздо проще и эффективнее просто напросто взглянуть, что конкретно фирма выполнить смогла. Итак, что мы успели за минувшие годы выполнить и кому именно оказывали помощь в перевозках и строительстве? В общем-то у нас в компании можно будет встретить большой выбор услуг, например как - [https://sab1.ru/takelage/pereezd-predpriyatiy переезд цеха] именно поэтому отметим только определенные, что выполнили успешно: - Когда требовалось устанавливать киоски «Союзпечать», именно мы выполняли эту работу; - Полноценное участие в строительстве «Экспоцентра»; - В столице ставили фонтаны, предприятия, памятники и другое. Кроме этого всего успешно сотрудничаем с сотнями различных застройщиков и компаний, а так же уже тысячами частников, когда необходимы профессиональные и недорогие услуги. Однако какие услуги предлагаем на сегодняшний момент? Сперва надо заметить, сможем обработать индивидуальный заказ. Однако если рассказать про наиболее популярные услуги, которые чаще всего заказывают у нас, то естественно надо заметить: - Перевозку товара; - Ремонт спецтехники; - Монтаж и демонтаж технического оборудования; - Разработку ППР. Вместе с тем нужно понимать, в своем собственном деле мы специалисты высокого уровня. К примеру сможем выполнить перевозку действительно любых грузов: бытовок, оборонной техники, металлоконструкций, станков и котельных. Обратившись в нашу организацию - [https://sab1.ru/ номер 1 спецавтобаза], можно будет не переживать, ведь берем на работу исключительно успешных мастеров, которые имеют необходимые знания. Все это позволяет назначить для заказчика выгодную цену, причем сделать работу в действительности быстро. Скажем в том случае, если требуется осуществить переезд фабрики, наши специалисты все выполнят сами, в том числе конечно же демонтаж. Не нужно волноваться насчет вида груза, сможем перевезти все, по сути без ограничений. Вся полезная информация размещена на интернет сайте, а обратившись к консультанту и описав собственную задачу, сможете узнать приблизительную цену. Итак, краткую информацию выложили, поэтому сами думайте, отправиться к лидеру, о котором знают почти все российские застройщики или же поискать начинающую компанию и немного рискнуть, в попытках сэкономить бюджет. 001aa15063942aad20a3b2318f2dd84cce87f34d Где возможно прямо сейчас купить уголки или сетку? 0 44 84 2023-01-27T18:38:04Z Sonnick84 30703286 Где возможно прямо сейчас купить уголки или сетку? wikitext text/x-wiki В случае если нужен металл, либо лес, то сейчас узнаете вы куда будет лучше позвонить! Фактически 10 лет работаем в своей сфере, предлагая заказчикам широкий каталог металла и сыпучих строительных материалов. За минувшие годы приобрели надежные партнерские отношения с уже тысячи заказчиков, а кроме того знаменитыми фирмами-застройщиками, оценившие комфортные цены, быструю доставку, а кроме этого хорошее качество товаров. Если рассказывать вкратце о нас, то нужно отметить широкий выбор металла: - Сетка дорожная; - Уголок; - Проволока; - Арматура; - Квадратная труба; - Швеллер. Это только лишь самые популярные и востребованные позиции, которые чаще всего приобретают клиенты. На самом деле ассортимент естественно намного больше и в том случае, если понадобится кому-либо [https://xn----itbbv1abddbs.xn--p1ai/ купить металл в Армавире], то сразу приходят в нашу фирму, либо попросту звонят. Следует заметить, не считая металла и сыпучих материалов, можем предоставить лес в обширном каталоге. Говорить подробнее о древесине не будем, поскольку на веб-сайте проще будет вам прочитать, там уже размещена подробная информация. Стоит отметить только несколько важных моментов, которые позволили нам стать лидерами. Итак, почему крупнейшие отечественные фирмы застройщики обращаются именно в нашу компанию? 1. Отличное качество всех изделий, и при этом по низким расценкам. 2. В действительности комфортные условия. 3. Грамотные сотрудники помогут вам оформить заявку и порекомендуют отличный вариант. 4. Можете заказать в общем-то любой необходимый объем, есть серьезный запас металла. Также подчеркнем, всегда четко соблюдаем сроки, прекрасно зная, что на текущий момент простой работников и строй техники выльется может в серьезные траты. Сможем произвести быструю доставку, в случае если понадобится лес или металл срочно. Но здесь нужно обсуждать с работником сайта, что вас проконсультирует конкретно по цене и срокам. В вопросе строительства важно подыскать проверенных партнеров, которые в срок готовы подвезти материалы. Это ценится на сегодняшний день намного больше низкой стоимости или удобных способов оплаты. Тем не менее, невзирая на быструю доставку, мы предоставляем также адекватные цены и лучшее качество. Всегда ценим собственных покупателей, поэтому на веб-сайте есть детальная информация о нашей компании, в том числе: ОГРН, контакты, адрес, ИНН, БИК банка и тд. 60267184ee837e0aa535f3537ecd00d3507bb5fc Огромный каталог лучших огнезащитных изделий 0 45 85 2023-02-03T10:39:54Z Sonnick84 30703286 Огромный каталог лучших огнезащитных изделий wikitext text/x-wiki Довольно известное предприятие, которое работает свыше 12 лет, предоставляет большой выбор теплоизоляционных и огнезащитных материалов собственного производства, а кроме этого других компаний. Главные преимущества этого предприятия: - Материалы в наличии всегда; - Профессиональные консультации; - Обширный выбор вариантов оплаты; - Детальное описание каждого материала; - Только лишь высококачественные материалы; - Ускоренная доставка. Пожалуй теперь вам понятно, благодаря чему на текущий момент представленное предприятие уже имеет сотни контрактов с известными отечественными застройщиками. При этом, в общем-то в случае если бы владельцы решились улучшить расценки на свою собственную продукцию, продажи шли. Тем не менее придерживаются тут принципа доступной цены, она в большинстве своем меньше, нежели могут предоставить другие интернет-магазины. В этом убедиться несложно, если понадобится купить [http://prominnovaciya.ru/ognezashchita-vozdukhovodov/ognezashchitnye-sosavy/pvk-2002 клей пвк 2002], не спеша просмотрите текущие предложения в разных магазинах, а затем откройте интернет-сайт предприятия. Сразу поймете где выгоднее приобрести. Известные застройщики сразу же регистрируют оптовые заказы. Трудности начинаются у частных спецов, потому что без значительного опыта, достаточно тяжело понять какие материалы подойдут. Поэтому потратили достаточно много сил и времени на формирование грамотной поддержки. Однако теперь каждый из клиентов возможность получает позвонить, после этого получить абсолютно бесплатную расширенную консультацию от эксперта. Он посоветует что лучше выбрать. Просто расскажите для чего решились заказать данный материал и где планируете его применить. В интернете масса довольных отзывов о представленной компании, где в отдельности упоминается высокая квалификация сотрудников. Помимо этого всего, изделия, представленные на веб-сайте, снабжены уже подробным описанием, а так же характеристиками. Представлены ключевые достоинства материала, как например экологичность, надежность, легкий монтаж или маленькая толщина. Обязательно прочитайте и о вариантах монтажа, особенно если думаете самостоятельно все сделать, не обращаясь к специалистам. Чтобы поговорить с оператором онлайн магазина, потребуется лишь позвонить или же отписаться на имейл. Все актуальные контакты выложены на веб-сайте, так что врядли возникнут сложности. Сотрудник проконсультирует, предоставит свои собственные рекомендации. 6bc46b2c5f9d13beea5297491351daae814704f8 Какими способами в наше время можно будет проверить номер телефона? 0 46 86 2023-02-04T08:50:41Z Sonnick84 30703286 Какими способами в наше время можно будет проверить номер телефона? wikitext text/x-wiki Сейчас есть самые разные варианты как можно проверить телефонный номер. Так к примеру возможно вбить номер в поисковик и взглянуть на выдачу, может будут какие-либо упоминания. Кроме этого можно открыть сайты знакомств, где написать в поиске номер телефона. Однако необходимо понимать, эффективность подобных способов небольшая и если действительно захотите выяснить все по номеру, нужно применить определенные ресурсы. Наш сайт дает возможность быстро проверить в общем-то любые телефонные номера бесплатно. Поиск выполняется в нескольких различных диапазонах, проверка же по миллионам веб-сайтов, включая DarkNet. Но сразу же надо заметить, в случае если понадобилось [https://getscam.com Пробить номер мобильного телефона], при этом абсолютно бесплатно, наш ресурс выдаст только системную информацию. В случае если необходимы подробности, надо будет заплатить за отчет. Причем тут необходимо сразу же дать объяснение, почему отчеты платные. Чтобы грамотно произвести проверку номера телефона, мы применяем мощнейшие сервера, дорогостоящий софт, а кроме того помощь экспертов. Конечно же это все расходы, при этом приличные. Тем не менее популярность сайта достаточно большая, поэтому и стоимость проверки номера недорогая. Как же проверить телефонный номер? Требуется вписать только непосредственно номер, после чего появится полноценный отчет, туда входят: - Упоминания и ссылки на любом сервисе; - Советы от проекта; - Жалобы. Конечно же сейчас кратко рассказали насчет отчета, так-то он гораздо обширнее. Выяснить, что именно там будет, сможете у нас на сервисе. Имеются похожие сайты, на которые можете обратиться в случае если нужно [https://abonentik.ru Найти кто звонил по номеру телефона], однако только мы на текущий день предоставляем в действительности детальный отчет. Хотя нужно четко понимать, если номер телефона "не засвечен" в сети интернет, отчет будет коротким. Тем не менее по своей собственной статистике наблюдаем, порядка 90% номеров проверяются не меньше 2-х раз от различных пользователей. Как правило это спамеры, либо мошенники, пытающиеся различными методами обмануть. Чтобы избежать подобного, потребуется использовать наш сайт. Иногда клиенты интересуются способами, как именно мы производим проверку телефонных номеров. Используется огромный ассортимент различных инструментов и баз, включая специфическую помощь иных проектов. В общем какие-то подробности говорить не будем. 2980e59364dab9549ee0ce1098da31771b9fb0c4 Надо оформить гражданство Италии или Австрии? 0 47 87 2023-02-10T20:00:53Z Sonnick84 30703286 Надо оформить гражданство Италии или Австрии? wikitext text/x-wiki Оформить гражданство какой-то другой страны, на сегодняшний момент заветная мечта многих. Многим нужно сделать ВНЖ России, другие хотят Германии или же США. Тем не менее выполнить это конечно же достаточно сложно. В общем-то существует 3 способа: • Отыскать специалистов, которые сумеют помочь в получении гражданство; • Позвонить частникам, они расскажут; • Собственноручно во-всем разобраться. Вероятнее всего вы отлично осознаете, самому зарегистрировать гражданство почти невозможно, особенно в странах Европы или же Штатах. Куда проще и в действительности дешевле будет отправиться к спецам, что уже давно в данной сфере работают и способны оказать помощь. Некоторые считают, что оформить ВНЖ Израиля или Европы фактически невозможно, нужны огромные средства. В целом вариантов регистрации гражданства достаточно много. Просто требуется эксперт, что сможет подобрать идеальный вариант именно вам. Понадобится зайти на к нам на веб сайт [https://garant.in/ https://garant.in/] и зарегистрировать заявку на совершенно бесплатную консультацию. Самое главное определите сначала паспорт, что вам нужен. Ну а специалист фирмы во-всем поможет! На собственном веб-сайте старались выложить полезную и ценную информацию, которая позволит лучше разобраться в оформлении ВНЖ. Прежде всего, это разумеется цена, потому что скорее всего потребуется использовать недвижимость. Другой же немаловажный момент - конечно время оформления. Надо понимать четко, получение ВНЖ процедура долгая. Существуют естественно варианты ускорения, однако здесь необходимо уже разговаривать с работником нашей компании. Какое гражданство мы готовы оформить своему клиенту? На текущий момент готовы предложить свыше 30 паспортов! Следует отметить наиболее популярные: Франция, Кипр, Израиль, Монако, США, Великобритания и Португалия. Хотя цена разумеется окажется довольно таки большой. Тем не менее в том случае, если действовать грамотно, можно прилично сэкономить. Главное позвонить в нашу компанию. В общем, если ищите [https://garant.in/ эмиграционный центр Garant.in], тогда нужно сразу же перейти на интернет сайт нашей компании и позвонить менеджеру, что проконсультирует и подробно все объяснит. Порою возникают проблемы с оформлением гражданства. Но на текущий момент, по статистике, свыше 98% клиентов успешно получают гражданство. И поэтому даже в том случае, если существуют у вас какие-то сомнения, что удастся зарегистрировать ВНЖ требуемой страны, позвоните - это абсолютно бесплатно! Существуют разнообразные методы получения гражданства, может быть получится подобрать что-то. 68cef01bc7e88f5114263261c7481a8c10352f2e Куда можно обратиться, в том случае, если нужно жилье купить? 0 48 88 2023-02-14T07:30:41Z Sonnick84 30703286 Куда можно обратиться, в том случае, если нужно жилье купить? wikitext text/x-wiki Сейчас немало разнообразных застройщиков могут предложить жилье приобрести. И при этом сэкономить можно будет, купив вторичное жилье, причем это имеет много преимуществ. Тем не менее если взглянуть на статистику, ясно станет, как раз новые многоквартирные дома обрели на сегодняшний день огромную востребованность и популярность, при этом стоимость в большинстве случаев чуть меньше "вторички". В общем-то цена зависит напрямую от застройщика, потому как если это серьезный исполнитель, то значит имеет уже готовые контракты с организациями, предлагающими спецтехнику, договора с разными производителями строительных материалов. Конечно же в итоге стоимость возведения ниже выходит, соответственно и заказчик получает квартиру по меньшей цене. Отметим, в том случае, если найти хорошего застройщика, возможно выбрать действительно комфортабельную квартиру. В принципе как раз подобные варианты мы предлагаем своим покупателям! Если поинтересоваться у покупателей нашей компании, почему они решились приобрести квартиру, то скажут: • Адекватные расценки; • Большой каталог разнообразных планировок; • Качественно разработанная ближайшая инфраструктура; • Можно будет использовать весьма выгодную ипотеку. Помимо этого всего стоит заметить, каждый заказчик анализирующий [https://gruppa-a.ru/contacts/ цены на квартиры в Ставрополе], имеет возможность выбрать оптимально подходящий для себя вариант по стоимости, а так же площади. Обратите внимание на планировку, поскольку качественно продумываем данный момент и сможем предложить огромный ассортимент разнообразных планировок. Многие клиенты применяют ипотеку, а так же мат капитал, что в действительности выгодное вложение финансов. Оператор вас проконсультирует по такому вопросу. В случае если необходима ипотека, отметим, на текущий день сотрудничаем с различными банками. На своем интернет сайте сразу же выложили текущие предложения, взгляните на проценты. Хотя разумеется надо понимать, точную сумму подсчитает только менеджер банка. Но консультацию провести мы также сумеем. Можно приобрести жилье заранее, в строящимся еще здании. При этом деньги получит застройщик только лишь по завершению работ. Так что касательно этого момента переживать не стоит. Кроме этого репутация хорошая, тысячи довольных собственников, что наконец сумели приобрести свою собственную квартиру! c650a87e106692a4a276e395d714093d7a4b0e3b Купить квартиру в известном ЖК Моне по выгодной цене 0 49 89 2023-02-15T15:00:11Z Sonnick84 30703286 Купить квартиру в известном ЖК Моне по выгодной цене wikitext text/x-wiki Приобретение квартиры задача всегда ответственная, в особенности учитывая стоимость. Именно поэтому неудивительно, что многие стремятся очень тщательно оценить застройщика, а только после оформлять покупку. В том случае, если планируете сэкономить время себе, порекомендуем не торопясь просмотреть данный краткий обзор, где мы расскажем про довольно таки знаменитого сейчас застройщика, а кроме этого его ЖК. Итак, наверное следует объяснить, как сможете сегодня квартиру приобрести и какие преимущества получить удастся. Стоит сказать про инфраструктуру. К примеру вблизи размещены: рынки, магазины, рестораны, садики, школы и кафе. Кроме всего этого имеется площадь, муз школа, центральный парк, театр и тд. И поэтому насчет инфраструктуры беспокоиться не следует, все в действительности с умом продумано. Если планируете узнать о самих квартирах, то назовем только следующие преимущества: - Консьерж-центр; - Подземный паркинг; - Самые разные планировки; - Автономное отопление. Стало интересно, хотите выяснить более подробно информацию об [https://xn--e1abmiff.xn--p1ai/ элитная квартира в Ставрополе], либо переговорить с менеджером компании? Зайдите на официальный интернет сайт, на котором размещена вся нужная информация, а кроме этого актуальные контакты консультанта. Многие клиенты получают вполне выгодные ставки процентов, что банки выдают для покупки своего жилья. Это так, возможно применить ипотеку от разнообразных банков, при этом проценты низкие. Но тут нужно переговорить с менеджером непосредственно самого банка, мы только можем посоветовать куда лучше позвонить. На самом непосредственно веб-сайте опубликованы фотоснимки, которые помогут оценить вам темп и качество строительства. Заметим, на текущий день работаем с лучшими строй компаниями, которые в действительности грамотно и качественно исполняют свою собственную работу. И поэтому по поводу срыва срока, волноваться не нужно. В собственном бизнесе мы являемся профессионалами, в чем сможете самолично удостовериться, прочитав отзывы или не торопясь изучив сайт. Мы честно осуществляем свою работу, на сайте опубликованы разрешительные документы, изучите их, если появится желание. Причем заметим, скорость строительства увеличивается, потому как клиенты, выяснив репутацию нашу, достаточно быстро скупают жилье в строящимся еще доме. И поэтому советуем не медлить и подыскать лучший вариант для самого себя. 299c7fa403e019001459d65be74e435c3a9b4281 Захотели оценить на самом деле качественный кальян? Ждем Вас! 0 50 90 2023-02-20T09:39:37Z Sonnick84 30703286 Захотели оценить на самом деле качественный кальян? Ждем Вас! wikitext text/x-wiki В случае если хочется так например заказать кальян, можно будет встретить множество разнообразных баров, в которых предоставляют огромный выбор вкусов. Но в случае если хочется действительно оценить классный кальян, забитый профессионалом, то естественно необходимо обращаться в известный на сегодняшний день бар! Сотрудники данного бара прекрасно на текущий момент разбираются в кальяне, благодаря чему могут создать в действительности шикарный аромат, и он гарантировано вам понравится! Заметим, имеется более 200 разных вкусов, причем их мастер клуба сможет подать в соло, либо создав потрясающий микс. Приходите и попросите забить кальян, сами поймете, почему все фанаты качественного кальяна приходят лишь в наш бар! Масса разных коктейлей, текила, виски, водка, шампанское, ром и другое - вот что сможет получить наш посетитель! Причем цены уже выложены на сайте, тем самым убедитесь, они в действительности вполне выгодные. Поэтому в бар возможно будет прийти компанией друзей, либо отметить свой день рождения и хорошо расслабиться с малым бюджетом. О блюдах можно будет говорить долго, потому что мы можем предложить обширный выбор. Однако пожалуй просто напросто отметим: - Большой выбор; - Шикарный вкус; - Цены доступные; - Готовят знаменитые шеф-повара. Непременно почитайте отзывы если интересует [https://blackmark.fun/malina Бронь столика Воронеж], потому как все наши гости остаются действительно благодарными. При этом любой сможет здесь встретить отдых по собственному вкусу. Имеются приставки с большим выбором видео-игр, интересные настолки, комфорт и многое другое. Регулярно проводим интересные мероприятия, зовя известных звезд эстрады, а кроме того различных диджеев. Оценить будущие выступления можете в афише у нас на веб сайте. Заметим, что нужно заранее стол бронировать, так как на популярное мероприятие, билеты выкупают за час-два. У нас в клубе возможно будет хорошо посидеть с друзьями, попросив кальян, алкоголь и конечно же вкуснейшую закуску, порубиться в настолку или приставки. Если хотите провести свидание, то вкусные десерты и различные коктейли позволят хорошо развлечься и получить множество незабываемых эмоций. Наши гости на самом деле остаются довольными, приезжайте и поймете сами из-за чего! 0ab998f24dac4cc3a67cbbb20198748bfba19565 Здесь только лучшие порно фанфики, рассказы и манги! 0 51 91 2023-02-23T17:53:22Z Sonnick84 30703286 Здесь только лучшие порно фанфики, рассказы и манги! wikitext text/x-wiki В наше время несмотря на огромное распространение эро видео в интернете, книги и рассказы по-прежнему довольно популярны и смогут предоставить собственному читателю самые разные истории, в большинстве своем трагичные или же комедийные, однако всегда с ярким, сочным сексуальным подтекстом. Истории и книги эротической тематики обрели высокую востребованность и популярность поскольку: - Гораздо ярче можно будет создать мир, применяя только фантазию читателей. - Можно будет загрузить лучшие в рейтинги истории, либо перейти на любимый веб сайт, где доступны [https://erolate.com/ истории 18+] совершенно бесплатно, не нужно регистрацию выполнять, либо платить что-либо. - Выбор на самом деле широкий, сотни самых разных жанров, включая: комедия, фэнтези, манга, фантастика, драма, сказка и тд. Если вы читали эротического содержания рассказы, тогда прекрасно осознаете, данные произведения распределить можно на 2-е группы: • Популярные, что созданы опытными авторами; • Любительские, в большинстве случаев увлекательные, тем не менее иногда читаются сложно. Поддерживая свой проект, на котором публикуются порно рассказы, сможем заявить: на сегодняшний день непосредственно любительские рассказы имеют большую популярность и востребованность. Более опытные писатели действуют по каким-то определенным правилам и создают в действительности интересные произведения. Любители же в основном правил по написаю книг не знают, по результату создают нечто весьма необычное. Может быть читается тяжело, однако сюжет порою действительно возбуждает. Именно поэтому стремимся поддерживать молодых писателей, что имеют возможность разместить свои собственные книги у нас на сайте и при этом заработать, в случае если разумеется рассказ понравится читателю. Как конкретно реализована система продаж объяснять в этом материале не станем. В случае если интересно, на интернет-сайте выложена подробная инструкция и разные рекомендации, которые помогут заработать на своих рассказах. Какие именно рассказы и книги можно будет встретить у нас на портале? Фанфики, манги, авторские, переводы, рассказы и многое иное. Поэтому в случае если обнаружите автора, что вам понравится, посоветуем сразу же его добавить в закладки, иначе после найти вновь среди огромного количества фанфиков, произведение его, будет довольно таки тяжело. Если вы хотите просмотреть только лучшие книги и рассказы, то используйте фильтр ТОП! 61e4aa4def557339d286830d600e178ee415edb4 Необходима помощь с ДЗ? Ждем Вас! 0 52 92 2023-02-26T16:49:44Z Sonnick84 30703286 Необходима помощь с ДЗ? Ждем Вас! wikitext text/x-wiki Сейчас многие специалисты заявляют, что образование, которое преподается в школах довольно низкое и в результате появляются самые разные сложности при поступлении в популярный университет. И поэтому услуги репетиторов получили на текущий момент огромную востребованность и популярность, хотя и обходятся недешево, и особенно в том случае, если нужна подготовка к университету, либо сдачи школьных ЕГЭ. Иногда ученик прекрасно разбирается в геометрии, химии, математики, информатики, физики, но напрочь не понимает немецкий или литературу. Кроме того стоит отметить педагогов, что часто в конфликте с рядом учеников. В итоге ученик попросту оказывается неспособным заработать хорошие отметки. Как быть? Имеется три в принципе варианта, которые различаются ценой: - Собственными силами подтянуть знания ученику; - Обратиться к профессионалам; - Подобрать хорошего репетитора. Скорее всего сами отлично знаете, что лучше отправиться к специалистам. Тем не менее где же отыскать организацию, в которой по комфортной стоимости сумеют на самом деле помочь экзамены сдать, поступить в популярный университет или попросту подтянуть знания? Почитайте отзывы в сети интернет, либо на официальном нашем сайте, на котором также размещены отзывы клиентов. Благодаря этому поймете о том, что мы в своей теме эксперты с многолетним опытом и сомнений в квалификации, не останется. Кроме этого на веб-сайте разместили информацию про своих преподавателей, в случае если интересно - прочитайте. В том случае, если коротко рассказать о нашем проекте, то можем повысить ребенку знания по различным дисциплинам: литература, физика, география, история, химия, русский, математика и так далее. Предоставляем два вида обучения: по расписанию, а кроме того разовое занятие. В общем-то отличное предложение, в случае если обратитесь в [https://pomogatordz.ru/ https://pomogatordz.ru/], потому как возможно единожды заплатив, рассчитывать на квалифицированную консультацию от профессионалов. Так например имеются проблемы с решением задач или пропустили важную тему. Для чего все-время платить за занятия, в том случае, если необходима в общем-то небольшая помощь, чтобы просто напросто рассказали как следует задачу решить. Посмотреть текущие цены вы можете на интернет-сайте, там так же имеется форма заказа. Подробно опишете, что именно требуется, а оператор предложит хороший вариант. fcb1b6efc9805e71c1b4a99373bdb4026b962d33 Важная информация о разных болезнях, а кроме того грамотном лечении 0 53 93 2023-03-13T18:45:57Z Sonnick84 30703286 Важная информация о разных болезнях, а кроме того грамотном лечении wikitext text/x-wiki Квалифицированные врачи утверждают - необходимо спортом заниматься, использовать лишь здоровую пищу. В результате избежать возможно будет разных болезней и недугов. Однако все-равно нужно осознавать, любой человек способен заболеть ОРЗ, ОРВИ, либо обычной простудой. Причем запускать данные заболевания чревато, иначе можно получить серьезные заболевания, например как трахеит, либо пневмонию, а их вылечить получится только с помощью мед препаратов и врача. Сегодня в сети интернет размещено немало информации, она позволяет самостоятельно разобраться в самых разных болезнях, их профилактики и конечно же способах лечения. Так например на нашем ресурсе, мы детально разобрали тему пневмонии, сказали как правильно ее лечить, что понадобится и тд. Хотя следует подчеркнуть, если появилась пневмония, либо к примеру бронхит, то заболевание уже запустили. У нас на сайте выложены самые разные материалы, что касаются иммунитета и здоровья, недугов и конечно методов лечения. В отдельности следует отметить спец материал - [https://bronchipret.by/suhoj-kashel-i-osobennosti-ego-lecheniya средство от сухого кашля], в котором врачи в подробностях описывают методы лечения, используемые препараты, а так же возможные последствия, в том случае, если недуг запустить. Касательно мед средств тоже старались подробно рассказать, но нужно понимать, лечение назначить сможет только опытный врач. В случае если к примеру стандартную простуду возможно будет и самому вылечить, то с пневмонией, подобные эксперименты чреваты большим проблемами. И поэтому предупреждаем участников своего веб-сайта - информация размещена только для ознакомления, препараты и лечение выпишет врач. Врач установит какая конкретно болезнь, а так же ее стадию, а это крайне важно при назначении мед средств. Применять серьезные уже антибиотики чревато потерей иммунитета. Но иногда данные препараты просто необходимы, особенно в том случае, если болезнь запущена уже. Также довольно таки часто, многие люди пройдя лишь часть лечения, перестают использовать мед препараты. В результате болезнь через время вновь возвращается, причем оказывается гораздо тяжелее. Всегда можно найти качественный дженерик или же альтернативу медицинского препарата, и особенно в случае если он знаменитого производителя и стоит дорого. Об этом также рассказали на собственном веб сайте. 7c1895489083ef6ac0c3583bcbfff2fe85e79986 Размещаем наиболее резонансные новости Питера 0 54 94 2023-03-20T09:44:00Z Sonnick84 30703286 Размещаем наиболее резонансные новости Питера wikitext text/x-wiki В наше время мир довольно активно меняется, так что не стоит удивляться, что сайты с новостями получили огромную посещаемость. Люди всегда ищут где найти можно правдивые новостные сводки о самых разных ситуациях в звездном мире, бизнесе, политике, экономике и многом другом. Вот только встретить качественный источник тяжело. Вот почему: - В наше время любой сайт-новостник нуждается обязательно во вложениях, а значит появляются "спонсоры"; - Сложно собрать опытных журналистов, а кроме этого грамотно наладить их взаимодействие; - Жесткая цензура. Конечно же, очень много сервисов, где каждую минуту выкладывают новости, ну а доход интернет-сайт дает лишь благодаря рекламе. Но в том случае, если не спеша посмотрите такие новости, узнаете, сотрудники просто копируют новостные сводки других СМИ, что имеют конечно же "инвесторов". Так что информации полно в сети интернет, но правдивую встретить сложно. Хорошо понимая такую проблему, решились создать новый портал, где публиковать объективные и честные новостные сводки. Конечно, сперва было довольно сложно, потому что без спонсоров, проект приносит лишь убытки. Тем не менее со-временем получилось решить это, путем применения релевантной рекламы, которая всегда интересна посетителю и при этом не раздражает. На сегодняшний день имеем уже значительный поток трафика, посетители действительно нам верят, понимая, что только лишь у нас возможно найти настоящие новости. По сути мы размещаем информацию на разнообразные темы, скажем как [https://piterskie-zametki.ru/224205 Перерасчёт пенсий когда стоит обращаться - новости], ну ну а выводы делает пользователь самостоятельно. Частенько видим попытку прикрыть сервис, потому что отказываемся от внешнего влияния. Но пригласили грамотных спецов, они дают возможность обойти блокировки. В случае если интересно стало выяснить поподробнее насчет нашего сервиса или просто напросто просмотреть честные новости - заходите, веб ссылку в сегодняшнем специальном обзоре выложили. Любой участник нашего ресурса прекрасно понимает, что в случае если пропустили мы какие-то увлекательные новостные сводки, всегда можно будет направить веб-ссылку нашему сотруднику, который опубликует данную новость. Но разумеется будет осуществлена проверка на фейки, так как это ключевое преимущество нашего новостника - честность. 670228df40e34a8694d07751703e49d275f15332 Где возможно будет выложить объявление по поводу продажи дома? 0 56 95 2023-03-21T11:00:18Z Sonnick84 30703286 Где возможно будет выложить объявление по поводу продажи дома? wikitext text/x-wiki При покупки квартиры или дома, нужно внимательным быть. Имеются тысячи разных нюансов и деталей, которые весьма важны. Правда возможно конечно же отправиться к риэлтору, что порекомендует подходящий вариант. Но это довольно таки рискованно, а кроме этого дороже, нежели самолично подыскать владельца. Писать в сегодняшнем спец обзоре о правильном выборе домов и квартир, преимуществ вторички и на что конкретно надо обращать свое внимание, не станем. Мы лучше подскажем отличный сайт, на котором вы сможете отыскать тысячи уже различных объявлений от собственников! Можно использовать знаменитые площадки, на которых выложены объявления касательно продажи недвижимости. Достаточно серьезные сервисы. Но их всех на сегодняшний день объединяет ряд очень важных главных недостатков: - Большое количество откровенных фейков; - Необходима авторизация; - Свыше 75% объявлений от риелторов; - Требуется часто платить для того, чтобы посмотреть контакты владельца. Сайт, который разработали мы, также разумеется имеет свои недостатки, однако преимуществ намного больше и главное - разработали простой интернет-сайт, на котором каждый возможность имеет размещать объявления или же их смотреть. Правда если планируете выложить свое собственное объявление, придется израсходовать немало времени, потому что понадобится указать: - Адрес; - Коммуникации; - Площадь; - Материал стен; - Удобства. Помимо этого всего порекомендуем в подробностях описать непосредственно саму жил площадь, в случае если вы хотите привлечь реальных покупателя. Наш сайт найти возможно в Google по фразе - [https://gurava.ru/geocities/51/%D0%9C%D0%B8%D1%85%D0%B0%D0%B9%D0%BB%D0%BE%D0%B2 недвижимость вторичка Михайлов Рязанская область Гурава.ру], либо просто выслать домен в избранное для того, чтобы потом сэкономить время себе. Но разумеется фактически наверняка, вначале решите крупные площадки использовать для покупки жилья. А когда вы устанете от бесчисленных риелторов, а кроме этого комиссий, перейдете на наш сервис! Говорить про ключевые преимущества веб-сайта не станем. Просто заметим: тут огромное количество объявлений, скопировать номера возможно абсолютно бесплатно, все грамотно и удобно сделано. Естественно, порой мошенники пробуют свои объявления размещать, однако разработали специальную систему, которая сама ищет подобные объявления и осуществляет их удаление. Помимо этого модераторы работу осуществляют, сразу же удаляя фейк объявления. 146b7264dcf37b5fde94feaac37f296ab237c18c Кто сегодня может создать декор из стеклофибробетона? 0 61 96 2023-03-23T07:10:48Z Sonnick84 30703286 Кто сегодня может создать декор из стеклофибробетона? wikitext text/x-wiki Существует немало разнообразных строительных материалов, различающихся долговечностью, качеством, прочностью, внешним видом, ценой и так далее. Это же сказать возможно будет насчет материалов, что используют для декора. Но если проанализировать эту тему подробнее, возможно будет выявить наиболее востребованные строй-материалы, которые чаще всего стараются все специалисты применять. Так например в случае если говорить по поводу фасадов, то любой успешный специалист сразу расскажет про уникальный в принципе строй материал, который многим известен. Именно из него в наше время делают фасады, что выглядят действительно потрясающе! Естественно мы говорим про стеклофибробетон! Более подробно почитать о непосредственно стеклофибробетоне можно в сети. Мы назовем только пять ключевых преимуществ, что дали возможность ему обрести высокую популярность. Это: • Долговечность; • Выгодная цена; • Легкость; • Элегантность; • Прочность. Тем не менее здесь понимать надо, если рассчитываете заказать отделку подобным материалом, необходимо подыскать надежного исполнителя, который имеет многолетний опыт, высококлассных специалистов и естественно свое производство. Всегда мы ценим своих собственных покупателей, а так же их время. Так что только перечислим основные достоинства нашей компании. Почитайте внимательно если вас интересует [https://berezka-decor.ru/ архитектурный декор фасада], сможете понять, почему мы лидеры на сегодняшний момент, невзирая на других исполнителей, которые стараются также получить репутацию. Ну а остальную информацию найдете на интернет сайте, веб ссылку на проект опубликуем в этом материале. В общем, почему покупатели выбирают нашу фирму: • Профессиональные монтажники, что знают разнообразные нюансы и мелочи в собственной работе; • Работаем уже более 15-ти лет и приобрели доверие многих знаменитых архитекторов и застройщиков; • Соблюдаем ГОСТы; • Огромные производственные мощности, а кроме того использование лучшего современного тех-оборудования; • Отказались полностью от посредников, что возможность дает установить весьма выгодную стоимость. Рекомендуем просто оценить на нашем веб-сайте примеры выполненных работ, врядли какие-то сомнения в опыте останутся у вас. О цене многие заказчики говорят, потому как она в действительности комфортная. Для этого напишите менеджеру, он подскажет текущие цены и поможет подобрать идеально подходящий вариант по вашему бюджету. 18710af4db13d436c22554b8e975360c8eff041d Хотите купить ром, джин или же водку по выгодной цене? 0 63 97 2023-03-23T13:32:29Z Sonnick84 30703286 Хотите купить ром, джин или же водку по выгодной цене? wikitext text/x-wiki Всегда возможно будет в интернет магазине заказать необходимый алкоголь, так как сегодня изготовители предлагают большой ассортимент, при этом цены тоже существенно различаются. Так что приобрести по низкой для себя цене алкоголь, возможно будет легко. Но в случае если касается вопрос большой партии спиртных напитков, возникают конечно вопросы и самый главный - где же найти подешевле? Есть самые разные магазины, в которых в принципе приобрести возможно будет алкоголь довольно таки дешево. Правда о качестве следует позабыть. В том случае, если ищите высокого качества алкоголь по низкой цене, то тогда рады представить наш знаменитый уже интернет-магазин! Описывать весь ассортимент алкогольных напитков, что мы предлагаем собственным заказчикам не будем. Довольно таки много различных позиций, имеющих большую востребованность и популярность. Однако большинство заявок от клиентов нам приходит на коньяк, водку и спирт. Разумеется качество высокое, а вся продукция отвечает ГОСТам. Касательно качества, стоимости и ассортимента мы уже рассказали. Но какие именно преимущества наш магазин имеет на текущий момент? В общем-то их довольно много, именно поэтому перечислим лишь ключевые: • Уникальные условия для покупателей, по дропшиппингу; • Доставка осуществляется оперативно при помощи почты; • Грамотные специалисты, которые проведут консультацию, а кроме того помогут оформить заказ; • Невзирая на низкие цены, проводятся разнообразные скидки и акции. Для того, чтобы снизить стоимость для собственных покупателей, мы решили, при этом достаточно давно, использовать дешевую тару, например как баклажки. Кроме этого всего подписали прямые контракты с изготовителями алкогольных напитков и постепенно, существенно расширив объем заказываемой алко продукции, получили хорошие скидки. Естественно, достичь этого всего было довольно тяжело вначале, но теперь в том случае, если клиента интересует как [https://alca-karobka.com/spirt-pshenichnaya-sleza этиловый спирт купить украина], спокойно сможет заказать в общем-то любой объем в нашем интернет магазине. Многие фирмы допускают обычную ошибку, когда экономят, снижают количество работников. У нас в магазине трудится немало спецов, таким образом заявки быстро обрабатываются и заказчик может оперативно получить алкоголь. На веб сайте детальное описание товаров, а так же отзывы. Также возможность получите после покупки в нашем интернет-магазине, выложить свое мнение и оценку. Это выполнить нетрудно, перейдите на выбранный товар и на странице внизу будет представлен специальный виджет, для составления своего отзыва. 0974e32be22f44bdf80fd8e8136c0dc95862c8b4 Туры по Казани, Краснодару, Белоруссии, Петербургу и Абхазии 0 64 98 2023-03-27T07:35:52Z Sonnick84 30703286 Туры по Казани, Краснодару, Белоруссии, Петербургу и Абхазии wikitext text/x-wiki Сейчас возможно хорошо развлечься и отдохнуть, выбрав тур-агентство. СНГ, Африка, Россия, Европа, Азия - везде доступны увлекательные и интересные туры, что точно понравятся. Кроме этого всего представлен пляжный, либо горнолыжный отдых, а можно отправиться с палатками или же в тур по рекам. В общем вариантов много и надо выбрать лишь проверенное турагентство, что обрело прекрасную репутацию, благодарные и положительные отзывы, может предложить огромный каталог разных направлений. Сегодня расскажем о своем собственном тур-агентстве. Ценим время клиентов, именно поэтому попробуем вкратце пройтись по главным достоинствам. Сначала подчеркнем, работаем с владельцами гостиниц, другими компаниями, авиакомпаниями, туроператорами, а кроме этого уже имеем индивидуальные предложения, для искушенных путешественников. В случае если просмотрите отзывы в интернете, выясните, мы в действительности лучшие. Это же в общем-то подтверждают на разных интернет-форумах, на которых туристы общаются. Но для того, чтобы достичь подобного уровня, естественно израсходовали многие годы и специалисты нашего агентства собственноручно проверили тысячи гостиниц, пытаясь сформировать предложения для разнообразных типов клиентов. Мы готовы предоставить туры фактически в любые страны. К примеру в том случае, если решите выбрать туры [https://arttur.info/product_category/avtobusom-k-moryu автобусом к морю турагентство], в итоге останетесь максимально довольным. Хотя подчеркнем, нужно сразу же выяснить, что конкретно хотите. Так например тур выбирать автобусом, если привыкли вы к комфорту, не рационально. Если обожаете горы, не надо заказывать туры на Бали. В принципе вероятно и сами это осознаете. Но консультант попытается в подробностях узнать о ваших предпочтениях, чтобы порекомендовать подходящий вариант. В том случае, если сказать о самых популярных и востребованных направлениях, то на текущий момент туры по РФ обрели очень высокую популярность. По сути поэтому примерно три года назад, мы приняли решение существенно расширить эти туры и сейчас готовы предложить обширный ассортимент направлений: • Кавказ; • Казань; • Карелия; • Петербург; • Крым. Кроме этого представлены более тематические туры, например как по циркам, достопримечательностям, театрам, крепостям, рекам и так далее. Расценки комфортные, однако значительно отличаются. Поэтому расскажите консультанту, что хотели увидеть, и он посоветует лучшие туры. 343231613464a21bead0976175e7ca0da8cd1bc7 Как можно сейчас самостоятельно вылечить фарингит? 0 66 99 2023-03-28T08:59:46Z Sonnick84 30703286 Как можно сейчас самостоятельно вылечить фарингит? wikitext text/x-wiki Почти каждый иногда болеет фарингитом, в общем-то это неудивительно, потому как само заболевание возникает из-за банальной простуды. Тем не менее есть и другие причины, скажем как: регулярное употребление горячей еды, напитков, воздействие дыма. Иногда фарингит проявляется из-за проблемы с сердцем. В принципе само заболевание можно будет вылечить самому, если использовать классические методы: - Чаще пить чай, морс, либо молочные напитки; - Употреблять больше воды; - Соблюдать правильное питание. Но все-же будет лучше обратиться к медику, потому что, как выше заметили, фарингит может быть лишь последствием серьезных заболеваний. В случае если игнорировать фарингит, могут вполне начаться уже более тяжелые проблемы, как например ухудшение иммунитета, воспаление трахеи, отеки, ну а это потребует уже оперативного вмешательства специалиста. Поэтому если не получится самому фарингит вылечить, запишитесь к врачам, которые посоветуют лекарство. На интернет-сайте постарались внимание уделить именно фарингиту, так как болезнь встречается часто. В нашей статье - [https://tonsilgon.by/tonzillit-kak-proyavlyaetsya-i-chem-lechit тонзиллит симптомы] подробным образом рассмотрены ключевые причины возникновения, способы лечения, а кроме этого описание последствий, что ждать, в том случае, если проигнорировать фарингит. В отдельности представлены советы и рекомендации врачей, которые уже имеют большой опыт и знают различные нюансы. Надо понимать, если фарингит появился из-за классической простуды, значит его вылечить можно будет самостоятельно. Но если фарингит является последствием серьезного заболевания, это чревато. Поэтому в случае если вы часто отмечаете першение, боль в горле и во рту или другие признаки фарингита, обращаться надо к врачу, чтобы проверил весь организм. По поводу медицинских препаратов нужно также быть осторожным, потому что можно использовать на основе натуральных ингредиентов, и при этом реализуются антибиотики, применять их стоит только при сложной форме фарингита. Так что лучше будет пойти к медику, который осуществит осмотр, в том случае, если требуется, после назначит все нужные анализы и составит подходящее лечение. Обязательно изучите информацию на интернет-сайте, в том случае, если желаете узнать подробнее насчет фарингита, методах лечения, а кроме того последствиях. Статьи написали опытные специалисты, поэтому доверять можно их рекомендациям. 5e632147f440b4c56ec77dd7337d09c4f5dac91d Ищите где купить франшизу на барбершоп? Можем помочь Вам! 0 67 100 2023-04-01T16:32:48Z Sonnick84 30703286 Ищите где купить франшизу на барбершоп? Можем помочь Вам! wikitext text/x-wiki В наше время по сути несложно открыть свой собственный бизнес. Во-первых, современное законодательство заметно облегчает процедуру регистрации для молодых бизнесменов, это помогает фактически без опыта, просто напросто просмотрев информацию в интернете, открыть свою собственную фирму. Имеются льготные тарифы, которые дают возможность существенно снизить расходы. Однако в любом случае нужно понимать, в том случае, если нет опыта, врядли вам удастся сразу же сделать действительно прибыльную фирму. В случае если посмотрите в сети советы предпринимателей, поймете, вероятность крах потерпеть очень высокая. Нужен план, а сделать его сложно. Но есть отличный вариант, который приобрел большую востребованность и популярность по всему миру. Естественно мы говорим насчет франшиз. Имеются различные франшизы, однако лучше найти фирму, предлагающую один готовый бизнес. Наверное объяснять почему нет смысла, итак отлично понимаете. Так например наша фирма хорошо разбирается в создании барбершопа, знает бесчисленные нюансы, а так же что потребуется. Вот почему на текущий день мы предлагаем по сути готовый бизнес. Обязательно следует отметить нюанс - мы даем гарантию! В случае если не удастся выйти на денежную прибыль через время, обратно вернем средства. Почему мы подобное предлагаем? Ответ прост: мы отлично разбираемся в подобном деле и знаем что необходимо для разработки на самом деле выгодного барбершопа. На нашем интернет сайте для клиента, в случае если его интересует [https://f-barbarossa.ru/ Лучшая франшиза барбершопа], доступна вся необходимая информация, включая: • Оборот; • Примерная прибыль; • Расчет окупаемости; • Видеокурс; • Необходимые инвестиции. Почитайте информацию на сайте, точно впечатлены окажетесь. Так например на сегодняшний день охват в социальных сетях больше 250000 участников. Проводим обучение каждого клиента, и туда входят занятия на практики. Почему именно барбершоп? На текущий момент это весьма удобный бизнес для новых предпринимателей, потому как инвестиций требуется не так много, все нюансы мы хорошо понимаем и сможем передать эти знания собственному ученику. А насчет дохода, можете узнать в сети интернет. Позвоните оператору, он осуществит совершенно бесплатную консультацию и разумеется даст ответы на все вопросы. 2b2ae59bc0a2427718c9a535ad7df707e1953222